{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "view-in-github",
        "colab_type": "text"
      },
      "source": [
        "<a href=\"https://colab.research.google.com/github/mrdbourke/tensorflow-deep-learning/blob/main/08_introduction_to_nlp_in_tensorflow.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "vtAgo5zYCClj"
      },
      "source": [
        "# 08. Natural Language Processing with TensorFlow\n",
        "\n",
        "![](https://raw.githubusercontent.com/mrdbourke/tensorflow-deep-learning/main/images/08-example-nlp-problems.png)\n",
        "*A handful of example natural language processing (NLP) and natural language understanding (NLU) problems. These are also often referred to as sequence problems (going from one sequence to another).*\n",
        "\n",
        "The main goal of [natural language processing (NLP)](https://becominghuman.ai/a-simple-introduction-to-natural-language-processing-ea66a1747b32) is to derive information from natural language.\n",
        "\n",
        "Natural language is a broad term but you can consider it to cover any of the following:\n",
        "* Text (such as that contained in an email, blog post, book, Tweet)\n",
        "* Speech (a conversation you have with a doctor, voice commands you give to a smart speaker)\n",
        "\n",
        "Under the umbrellas of text and speech there are many different things you might want to do.\n",
        "\n",
        "If you're building an email application, you might want to scan incoming emails to see if they're spam or not spam (classification).\n",
        "\n",
        "If you're trying to analyse customer feedback complaints, you might want to discover which section of your business they're for.\n",
        "\n",
        "> 🔑 **Note:** Both of these types of data are often referred to as *sequences* (a sentence is a sequence of words). So a common term you'll come across in NLP problems is called *seq2seq*, in other words, finding information in one sequence to produce another sequence (e.g. converting a speech command to a sequence of text-based steps).\n",
        "\n",
        "To get hands-on with NLP in TensorFlow, we're going to practice the steps we've used previously but this time with text data:\n",
        "\n",
        "```\n",
        "Text -> turn into numbers -> build a model -> train the model to find patterns -> use patterns (make predictions)\n",
        "```\n",
        "\n",
        "> 📖 **Resource:** For a great overview of NLP and the different problems within it, read the article [*A Simple Introduction to Natural Language Processing*](https://becominghuman.ai/a-simple-introduction-to-natural-language-processing-ea66a1747b32).\n",
        "\n",
        "## What we're going to cover\n",
        "\n",
        "Let's get specific hey?\n",
        "\n",
        "* Downloading a text dataset\n",
        "* Visualizing text data\n",
        "* Converting text into numbers using tokenization\n",
        "* Turning our tokenized text into an embedding\n",
        "* Modelling a text dataset\n",
        "  * Starting with a baseline (TF-IDF)\n",
        "  * Building several deep learning text models\n",
        "    * Dense, LSTM, GRU, Conv1D, Transfer learning\n",
        "* Comparing the performance of each our models\n",
        "* Combining our models into an ensemble\n",
        "* Saving and loading a trained model\n",
        "* Find the most wrong predictions\n",
        "\n",
        "## How you should approach this notebook\n",
        "\n",
        "You can read through the descriptions and the code (it should all run, except for the cells which error on purpose), but there's a better option.\n",
        "\n",
        "Write all of the code yourself.\n",
        "\n",
        "Yes. I'm serious. Create a new notebook, and rewrite each line by yourself. Investigate it, see if you can break it, why does it break?\n",
        "\n",
        "You don't have to write the text descriptions but writing the code yourself is a great way to get hands-on experience.\n",
        "\n",
        "Don't worry if you make mistakes, we all do. The way to get better and make less mistakes is to write more code.\n",
        "\n",
        "> 📖 **Resource:** See the full set of course materials on GitHub: https://github.com/mrdbourke/tensorflow-deep-learning"
      ]
    },
    {
      "cell_type": "code",
      "source": [
        "import datetime\n",
        "print(f\"Notebook last run (end-to-end): {datetime.datetime.now()}\")"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "PTHLj4iNaAFW",
        "outputId": "21cb2292-e993-4510-8ba2-657c858b85a5"
      },
      "execution_count": 1,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Notebook last run (end-to-end): 2023-05-26 00:14:41.453244\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "4Zh2N1hZtvpN"
      },
      "source": [
        "## Check for GPU\n",
        "\n",
        "In order for our deep learning models to run as fast as possible, we'll need access to a GPU.\n",
        "\n",
        "In Google Colab, you can set this up by going to Runtime -> Change runtime type -> Hardware accelerator -> GPU.\n",
        "\n",
        "After selecting GPU, you may have to restart the runtime."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 2,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "DEYTFigmc3CI",
        "outputId": "366e1900-ad12-4742-9410-8881fdd74ec1"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "GPU 0: NVIDIA A100-SXM4-40GB (UUID: GPU-a07b6e3e-3ef6-217b-d41f-dc5c4d6babfd)\n"
          ]
        }
      ],
      "source": [
        "# Check for GPU\n",
        "!nvidia-smi -L"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "gS3YnNNI8oFk"
      },
      "source": [
        "## Get helper functions\n",
        "\n",
        "In past modules, we've created a bunch of helper functions to do small tasks required for our notebooks.\n",
        "\n",
        "Rather than rewrite all of these, we can import a script and load them in from there.\n",
        "\n",
        "The script containing our helper functions can be [found on GitHub](https://github.com/mrdbourke/tensorflow-deep-learning/blob/main/extras/helper_functions.py)."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 3,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "aFOHPqgE8pv-",
        "outputId": "b2e44a19-8e5b-46e8-927b-f32b6b09c398"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "--2023-05-26 00:14:41--  https://raw.githubusercontent.com/mrdbourke/tensorflow-deep-learning/main/extras/helper_functions.py\n",
            "Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.108.133, 185.199.109.133, 185.199.110.133, ...\n",
            "Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.108.133|:443... connected.\n",
            "HTTP request sent, awaiting response... 200 OK\n",
            "Length: 10246 (10K) [text/plain]\n",
            "Saving to: ‘helper_functions.py’\n",
            "\n",
            "helper_functions.py 100%[===================>]  10.01K  --.-KB/s    in 0s      \n",
            "\n",
            "2023-05-26 00:14:41 (94.5 MB/s) - ‘helper_functions.py’ saved [10246/10246]\n",
            "\n"
          ]
        }
      ],
      "source": [
        "# Download helper functions script\n",
        "!wget https://raw.githubusercontent.com/mrdbourke/tensorflow-deep-learning/main/extras/helper_functions.py"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 4,
      "metadata": {
        "id": "ICFbSkoM85tq"
      },
      "outputs": [],
      "source": [
        "# Import series of helper functions for the notebook\n",
        "from helper_functions import unzip_data, create_tensorboard_callback, plot_loss_curves, compare_historys"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "cCZrclc2COWW"
      },
      "source": [
        "## Download a text dataset\n",
        "\n",
        "Let's start by download a text dataset. We'll be using the [Real or Not?](https://www.kaggle.com/c/nlp-getting-started/data) dataset from Kaggle which contains text-based Tweets about natural disasters. \n",
        "\n",
        "The Real Tweets are actually about disasters, for example:\n",
        "\n",
        "```\n",
        "Jetstar and Virgin forced to cancel Bali flights again because of ash from Mount Raung volcano\n",
        "```\n",
        "\n",
        "The Not Real Tweets are Tweets not about disasters (they can be on anything), for example:\n",
        "\n",
        "```\n",
        "'Education is the most powerful weapon which you can use to change the world.' Nelson #Mandela #quote\n",
        "```\n",
        "\n",
        "For convenience, the dataset has been [downloaded from Kaggle](https://www.kaggle.com/c/nlp-getting-started/data) (doing this requires a Kaggle account) and uploaded as a downloadable zip file. \n",
        "\n",
        "> 🔑 **Note:** The original downloaded data has not been altered to how you would download it from Kaggle."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 5,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "C0FEcci5IH8S",
        "outputId": "fe57de93-0d24-46a7-9927-1aa2ea53b877"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "--2023-05-26 00:14:45--  https://storage.googleapis.com/ztm_tf_course/nlp_getting_started.zip\n",
            "Resolving storage.googleapis.com (storage.googleapis.com)... 172.217.194.128, 74.125.200.128, 74.125.68.128, ...\n",
            "Connecting to storage.googleapis.com (storage.googleapis.com)|172.217.194.128|:443... connected.\n",
            "HTTP request sent, awaiting response... 200 OK\n",
            "Length: 607343 (593K) [application/zip]\n",
            "Saving to: ‘nlp_getting_started.zip’\n",
            "\n",
            "nlp_getting_started 100%[===================>] 593.11K   731KB/s    in 0.8s    \n",
            "\n",
            "2023-05-26 00:14:46 (731 KB/s) - ‘nlp_getting_started.zip’ saved [607343/607343]\n",
            "\n"
          ]
        }
      ],
      "source": [
        "# Download data (same as from Kaggle)\n",
        "!wget \"https://storage.googleapis.com/ztm_tf_course/nlp_getting_started.zip\"\n",
        "\n",
        "# Unzip data\n",
        "unzip_data(\"nlp_getting_started.zip\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "wBIR6tTI9QcR"
      },
      "source": [
        "Unzipping `nlp_getting_started.zip` gives the following 3 `.csv` files:\n",
        "* `sample_submission.csv` - an example of the file you'd submit to the Kaggle competition of your model's predictions.\n",
        "* `train.csv` - training samples of real and not real diaster Tweets.\n",
        "* `test.csv` - testing samples of real and not real diaster Tweets."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "7HpxZKYdD6V-"
      },
      "source": [
        "## Visualizing a text dataset\n",
        "\n",
        "Once you've acquired a new dataset to work with, what should you do first?\n",
        "\n",
        "Explore it? Inspect it? Verify it? Become one with it?\n",
        "\n",
        "All correct.\n",
        "\n",
        "Remember the motto: visualize, visualize, visualize.\n",
        "\n",
        "Right now, our text data samples are in the form of `.csv` files. For an easy way to make them visual, let's turn them into pandas DataFrame's.\n",
        "\n",
        "> 📖 **Reading:** You might come across text datasets in many different formats. Aside from CSV files (what we're working with), you'll probably encounter `.txt` files and `.json` files too. For working with these type of files, I'd recommend reading the two following articles by RealPython:\n",
        "* [How to Read and Write Files in Python](https://realpython.com/read-write-files-python/)\n",
        "* [Working with JSON Data in Python](https://realpython.com/python-json/)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 6,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 206
        },
        "id": "qRvkeYEJIKsw",
        "outputId": "28e2fd66-1a50-4685-94b0-1f57bf7e5968"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "   id keyword location                                               text  \\\n",
              "0   1     NaN      NaN  Our Deeds are the Reason of this #earthquake M...   \n",
              "1   4     NaN      NaN             Forest fire near La Ronge Sask. Canada   \n",
              "2   5     NaN      NaN  All residents asked to 'shelter in place' are ...   \n",
              "3   6     NaN      NaN  13,000 people receive #wildfires evacuation or...   \n",
              "4   7     NaN      NaN  Just got sent this photo from Ruby #Alaska as ...   \n",
              "\n",
              "   target  \n",
              "0       1  \n",
              "1       1  \n",
              "2       1  \n",
              "3       1  \n",
              "4       1  "
            ],
            "text/html": [
              "\n",
              "  <div id=\"df-276e93fe-6894-49de-8a77-1941d3ab7667\">\n",
              "    <div class=\"colab-df-container\">\n",
              "      <div>\n",
              "<style scoped>\n",
              "    .dataframe tbody tr th:only-of-type {\n",
              "        vertical-align: middle;\n",
              "    }\n",
              "\n",
              "    .dataframe tbody tr th {\n",
              "        vertical-align: top;\n",
              "    }\n",
              "\n",
              "    .dataframe thead th {\n",
              "        text-align: right;\n",
              "    }\n",
              "</style>\n",
              "<table border=\"1\" class=\"dataframe\">\n",
              "  <thead>\n",
              "    <tr style=\"text-align: right;\">\n",
              "      <th></th>\n",
              "      <th>id</th>\n",
              "      <th>keyword</th>\n",
              "      <th>location</th>\n",
              "      <th>text</th>\n",
              "      <th>target</th>\n",
              "    </tr>\n",
              "  </thead>\n",
              "  <tbody>\n",
              "    <tr>\n",
              "      <th>0</th>\n",
              "      <td>1</td>\n",
              "      <td>NaN</td>\n",
              "      <td>NaN</td>\n",
              "      <td>Our Deeds are the Reason of this #earthquake M...</td>\n",
              "      <td>1</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>1</th>\n",
              "      <td>4</td>\n",
              "      <td>NaN</td>\n",
              "      <td>NaN</td>\n",
              "      <td>Forest fire near La Ronge Sask. Canada</td>\n",
              "      <td>1</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>2</th>\n",
              "      <td>5</td>\n",
              "      <td>NaN</td>\n",
              "      <td>NaN</td>\n",
              "      <td>All residents asked to 'shelter in place' are ...</td>\n",
              "      <td>1</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>3</th>\n",
              "      <td>6</td>\n",
              "      <td>NaN</td>\n",
              "      <td>NaN</td>\n",
              "      <td>13,000 people receive #wildfires evacuation or...</td>\n",
              "      <td>1</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>4</th>\n",
              "      <td>7</td>\n",
              "      <td>NaN</td>\n",
              "      <td>NaN</td>\n",
              "      <td>Just got sent this photo from Ruby #Alaska as ...</td>\n",
              "      <td>1</td>\n",
              "    </tr>\n",
              "  </tbody>\n",
              "</table>\n",
              "</div>\n",
              "      <button class=\"colab-df-convert\" onclick=\"convertToInteractive('df-276e93fe-6894-49de-8a77-1941d3ab7667')\"\n",
              "              title=\"Convert this dataframe to an interactive table.\"\n",
              "              style=\"display:none;\">\n",
              "        \n",
              "  <svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\"viewBox=\"0 0 24 24\"\n",
              "       width=\"24px\">\n",
              "    <path d=\"M0 0h24v24H0V0z\" fill=\"none\"/>\n",
              "    <path d=\"M18.56 5.44l.94 2.06.94-2.06 2.06-.94-2.06-.94-.94-2.06-.94 2.06-2.06.94zm-11 1L8.5 8.5l.94-2.06 2.06-.94-2.06-.94L8.5 2.5l-.94 2.06-2.06.94zm10 10l.94 2.06.94-2.06 2.06-.94-2.06-.94-.94-2.06-.94 2.06-2.06.94z\"/><path d=\"M17.41 7.96l-1.37-1.37c-.4-.4-.92-.59-1.43-.59-.52 0-1.04.2-1.43.59L10.3 9.45l-7.72 7.72c-.78.78-.78 2.05 0 2.83L4 21.41c.39.39.9.59 1.41.59.51 0 1.02-.2 1.41-.59l7.78-7.78 2.81-2.81c.8-.78.8-2.07 0-2.86zM5.41 20L4 18.59l7.72-7.72 1.47 1.35L5.41 20z\"/>\n",
              "  </svg>\n",
              "      </button>\n",
              "      \n",
              "  <style>\n",
              "    .colab-df-container {\n",
              "      display:flex;\n",
              "      flex-wrap:wrap;\n",
              "      gap: 12px;\n",
              "    }\n",
              "\n",
              "    .colab-df-convert {\n",
              "      background-color: #E8F0FE;\n",
              "      border: none;\n",
              "      border-radius: 50%;\n",
              "      cursor: pointer;\n",
              "      display: none;\n",
              "      fill: #1967D2;\n",
              "      height: 32px;\n",
              "      padding: 0 0 0 0;\n",
              "      width: 32px;\n",
              "    }\n",
              "\n",
              "    .colab-df-convert:hover {\n",
              "      background-color: #E2EBFA;\n",
              "      box-shadow: 0px 1px 2px rgba(60, 64, 67, 0.3), 0px 1px 3px 1px rgba(60, 64, 67, 0.15);\n",
              "      fill: #174EA6;\n",
              "    }\n",
              "\n",
              "    [theme=dark] .colab-df-convert {\n",
              "      background-color: #3B4455;\n",
              "      fill: #D2E3FC;\n",
              "    }\n",
              "\n",
              "    [theme=dark] .colab-df-convert:hover {\n",
              "      background-color: #434B5C;\n",
              "      box-shadow: 0px 1px 3px 1px rgba(0, 0, 0, 0.15);\n",
              "      filter: drop-shadow(0px 1px 2px rgba(0, 0, 0, 0.3));\n",
              "      fill: #FFFFFF;\n",
              "    }\n",
              "  </style>\n",
              "\n",
              "      <script>\n",
              "        const buttonEl =\n",
              "          document.querySelector('#df-276e93fe-6894-49de-8a77-1941d3ab7667 button.colab-df-convert');\n",
              "        buttonEl.style.display =\n",
              "          google.colab.kernel.accessAllowed ? 'block' : 'none';\n",
              "\n",
              "        async function convertToInteractive(key) {\n",
              "          const element = document.querySelector('#df-276e93fe-6894-49de-8a77-1941d3ab7667');\n",
              "          const dataTable =\n",
              "            await google.colab.kernel.invokeFunction('convertToInteractive',\n",
              "                                                     [key], {});\n",
              "          if (!dataTable) return;\n",
              "\n",
              "          const docLinkHtml = 'Like what you see? Visit the ' +\n",
              "            '<a target=\"_blank\" href=https://colab.research.google.com/notebooks/data_table.ipynb>data table notebook</a>'\n",
              "            + ' to learn more about interactive tables.';\n",
              "          element.innerHTML = '';\n",
              "          dataTable['output_type'] = 'display_data';\n",
              "          await google.colab.output.renderOutput(dataTable, element);\n",
              "          const docLink = document.createElement('div');\n",
              "          docLink.innerHTML = docLinkHtml;\n",
              "          element.appendChild(docLink);\n",
              "        }\n",
              "      </script>\n",
              "    </div>\n",
              "  </div>\n",
              "  "
            ]
          },
          "metadata": {},
          "execution_count": 6
        }
      ],
      "source": [
        "# Turn .csv files into pandas DataFrame's\n",
        "import pandas as pd\n",
        "train_df = pd.read_csv(\"train.csv\")\n",
        "test_df = pd.read_csv(\"test.csv\")\n",
        "train_df.head()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "1xGqlnQaLmaT"
      },
      "source": [
        "The training data we downloaded is probably shuffled already. But just to be sure, let's shuffle it again."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 7,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 206
        },
        "id": "ACCE7h6OMVjR",
        "outputId": "ce040217-1a4b-4199-e446-f4b87988d90d"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "        id      keyword               location  \\\n",
              "2644  3796  destruction                    NaN   \n",
              "2227  3185       deluge                    NaN   \n",
              "5448  7769       police                     UK   \n",
              "132    191   aftershock                    NaN   \n",
              "6845  9810       trauma  Montgomery County, MD   \n",
              "\n",
              "                                                   text  target  \n",
              "2644  So you have a new weapon that can cause un-ima...       1  \n",
              "2227  The f$&amp;@ing things I do for #GISHWHES Just...       0  \n",
              "5448  DT @georgegalloway: RT @Galloway4Mayor: ÛÏThe...       1  \n",
              "132   Aftershock back to school kick off was great. ...       0  \n",
              "6845  in response to trauma Children of Addicts deve...       0  "
            ],
            "text/html": [
              "\n",
              "  <div id=\"df-5e31a1d7-1696-45a3-bd44-8f0a99e30d9f\">\n",
              "    <div class=\"colab-df-container\">\n",
              "      <div>\n",
              "<style scoped>\n",
              "    .dataframe tbody tr th:only-of-type {\n",
              "        vertical-align: middle;\n",
              "    }\n",
              "\n",
              "    .dataframe tbody tr th {\n",
              "        vertical-align: top;\n",
              "    }\n",
              "\n",
              "    .dataframe thead th {\n",
              "        text-align: right;\n",
              "    }\n",
              "</style>\n",
              "<table border=\"1\" class=\"dataframe\">\n",
              "  <thead>\n",
              "    <tr style=\"text-align: right;\">\n",
              "      <th></th>\n",
              "      <th>id</th>\n",
              "      <th>keyword</th>\n",
              "      <th>location</th>\n",
              "      <th>text</th>\n",
              "      <th>target</th>\n",
              "    </tr>\n",
              "  </thead>\n",
              "  <tbody>\n",
              "    <tr>\n",
              "      <th>2644</th>\n",
              "      <td>3796</td>\n",
              "      <td>destruction</td>\n",
              "      <td>NaN</td>\n",
              "      <td>So you have a new weapon that can cause un-ima...</td>\n",
              "      <td>1</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>2227</th>\n",
              "      <td>3185</td>\n",
              "      <td>deluge</td>\n",
              "      <td>NaN</td>\n",
              "      <td>The f$&amp;amp;@ing things I do for #GISHWHES Just...</td>\n",
              "      <td>0</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>5448</th>\n",
              "      <td>7769</td>\n",
              "      <td>police</td>\n",
              "      <td>UK</td>\n",
              "      <td>DT @georgegalloway: RT @Galloway4Mayor: ÛÏThe...</td>\n",
              "      <td>1</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>132</th>\n",
              "      <td>191</td>\n",
              "      <td>aftershock</td>\n",
              "      <td>NaN</td>\n",
              "      <td>Aftershock back to school kick off was great. ...</td>\n",
              "      <td>0</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>6845</th>\n",
              "      <td>9810</td>\n",
              "      <td>trauma</td>\n",
              "      <td>Montgomery County, MD</td>\n",
              "      <td>in response to trauma Children of Addicts deve...</td>\n",
              "      <td>0</td>\n",
              "    </tr>\n",
              "  </tbody>\n",
              "</table>\n",
              "</div>\n",
              "      <button class=\"colab-df-convert\" onclick=\"convertToInteractive('df-5e31a1d7-1696-45a3-bd44-8f0a99e30d9f')\"\n",
              "              title=\"Convert this dataframe to an interactive table.\"\n",
              "              style=\"display:none;\">\n",
              "        \n",
              "  <svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\"viewBox=\"0 0 24 24\"\n",
              "       width=\"24px\">\n",
              "    <path d=\"M0 0h24v24H0V0z\" fill=\"none\"/>\n",
              "    <path d=\"M18.56 5.44l.94 2.06.94-2.06 2.06-.94-2.06-.94-.94-2.06-.94 2.06-2.06.94zm-11 1L8.5 8.5l.94-2.06 2.06-.94-2.06-.94L8.5 2.5l-.94 2.06-2.06.94zm10 10l.94 2.06.94-2.06 2.06-.94-2.06-.94-.94-2.06-.94 2.06-2.06.94z\"/><path d=\"M17.41 7.96l-1.37-1.37c-.4-.4-.92-.59-1.43-.59-.52 0-1.04.2-1.43.59L10.3 9.45l-7.72 7.72c-.78.78-.78 2.05 0 2.83L4 21.41c.39.39.9.59 1.41.59.51 0 1.02-.2 1.41-.59l7.78-7.78 2.81-2.81c.8-.78.8-2.07 0-2.86zM5.41 20L4 18.59l7.72-7.72 1.47 1.35L5.41 20z\"/>\n",
              "  </svg>\n",
              "      </button>\n",
              "      \n",
              "  <style>\n",
              "    .colab-df-container {\n",
              "      display:flex;\n",
              "      flex-wrap:wrap;\n",
              "      gap: 12px;\n",
              "    }\n",
              "\n",
              "    .colab-df-convert {\n",
              "      background-color: #E8F0FE;\n",
              "      border: none;\n",
              "      border-radius: 50%;\n",
              "      cursor: pointer;\n",
              "      display: none;\n",
              "      fill: #1967D2;\n",
              "      height: 32px;\n",
              "      padding: 0 0 0 0;\n",
              "      width: 32px;\n",
              "    }\n",
              "\n",
              "    .colab-df-convert:hover {\n",
              "      background-color: #E2EBFA;\n",
              "      box-shadow: 0px 1px 2px rgba(60, 64, 67, 0.3), 0px 1px 3px 1px rgba(60, 64, 67, 0.15);\n",
              "      fill: #174EA6;\n",
              "    }\n",
              "\n",
              "    [theme=dark] .colab-df-convert {\n",
              "      background-color: #3B4455;\n",
              "      fill: #D2E3FC;\n",
              "    }\n",
              "\n",
              "    [theme=dark] .colab-df-convert:hover {\n",
              "      background-color: #434B5C;\n",
              "      box-shadow: 0px 1px 3px 1px rgba(0, 0, 0, 0.15);\n",
              "      filter: drop-shadow(0px 1px 2px rgba(0, 0, 0, 0.3));\n",
              "      fill: #FFFFFF;\n",
              "    }\n",
              "  </style>\n",
              "\n",
              "      <script>\n",
              "        const buttonEl =\n",
              "          document.querySelector('#df-5e31a1d7-1696-45a3-bd44-8f0a99e30d9f button.colab-df-convert');\n",
              "        buttonEl.style.display =\n",
              "          google.colab.kernel.accessAllowed ? 'block' : 'none';\n",
              "\n",
              "        async function convertToInteractive(key) {\n",
              "          const element = document.querySelector('#df-5e31a1d7-1696-45a3-bd44-8f0a99e30d9f');\n",
              "          const dataTable =\n",
              "            await google.colab.kernel.invokeFunction('convertToInteractive',\n",
              "                                                     [key], {});\n",
              "          if (!dataTable) return;\n",
              "\n",
              "          const docLinkHtml = 'Like what you see? Visit the ' +\n",
              "            '<a target=\"_blank\" href=https://colab.research.google.com/notebooks/data_table.ipynb>data table notebook</a>'\n",
              "            + ' to learn more about interactive tables.';\n",
              "          element.innerHTML = '';\n",
              "          dataTable['output_type'] = 'display_data';\n",
              "          await google.colab.output.renderOutput(dataTable, element);\n",
              "          const docLink = document.createElement('div');\n",
              "          docLink.innerHTML = docLinkHtml;\n",
              "          element.appendChild(docLink);\n",
              "        }\n",
              "      </script>\n",
              "    </div>\n",
              "  </div>\n",
              "  "
            ]
          },
          "metadata": {},
          "execution_count": 7
        }
      ],
      "source": [
        "# Shuffle training dataframe\n",
        "train_df_shuffled = train_df.sample(frac=1, random_state=42) # shuffle with random_state=42 for reproducibility\n",
        "train_df_shuffled.head()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "Lw4mKW1yL0kI"
      },
      "source": [
        "Notice how the training data has a `\"target\"` column.\n",
        "\n",
        "We're going to be writing code to find patterns (e.g. different combinations of words) in the `\"text\"` column of the training dataset to predict the value of the `\"target\"` column.\n",
        "\n",
        "The test dataset doesn't have a `\"target\"` column.\n",
        "\n",
        "```\n",
        "Inputs (text column) -> Machine Learning Algorithm -> Outputs (target column)\n",
        "```\n",
        "\n",
        "![](https://raw.githubusercontent.com/mrdbourke/tensorflow-deep-learning/main/images/08-text-classification-inputs-and-outputs.png)\n",
        "*Example text classification inputs and outputs for the problem of classifying whether a Tweet is about a disaster or not.*"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 8,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 206
        },
        "id": "tDh5t7thI5BM",
        "outputId": "4b7504c3-926c-47fe-94d7-ef4adbda02fe"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "   id keyword location                                               text\n",
              "0   0     NaN      NaN                 Just happened a terrible car crash\n",
              "1   2     NaN      NaN  Heard about #earthquake is different cities, s...\n",
              "2   3     NaN      NaN  there is a forest fire at spot pond, geese are...\n",
              "3   9     NaN      NaN           Apocalypse lighting. #Spokane #wildfires\n",
              "4  11     NaN      NaN      Typhoon Soudelor kills 28 in China and Taiwan"
            ],
            "text/html": [
              "\n",
              "  <div id=\"df-f8ced319-efb8-48fd-a908-0a300f8b2aab\">\n",
              "    <div class=\"colab-df-container\">\n",
              "      <div>\n",
              "<style scoped>\n",
              "    .dataframe tbody tr th:only-of-type {\n",
              "        vertical-align: middle;\n",
              "    }\n",
              "\n",
              "    .dataframe tbody tr th {\n",
              "        vertical-align: top;\n",
              "    }\n",
              "\n",
              "    .dataframe thead th {\n",
              "        text-align: right;\n",
              "    }\n",
              "</style>\n",
              "<table border=\"1\" class=\"dataframe\">\n",
              "  <thead>\n",
              "    <tr style=\"text-align: right;\">\n",
              "      <th></th>\n",
              "      <th>id</th>\n",
              "      <th>keyword</th>\n",
              "      <th>location</th>\n",
              "      <th>text</th>\n",
              "    </tr>\n",
              "  </thead>\n",
              "  <tbody>\n",
              "    <tr>\n",
              "      <th>0</th>\n",
              "      <td>0</td>\n",
              "      <td>NaN</td>\n",
              "      <td>NaN</td>\n",
              "      <td>Just happened a terrible car crash</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>1</th>\n",
              "      <td>2</td>\n",
              "      <td>NaN</td>\n",
              "      <td>NaN</td>\n",
              "      <td>Heard about #earthquake is different cities, s...</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>2</th>\n",
              "      <td>3</td>\n",
              "      <td>NaN</td>\n",
              "      <td>NaN</td>\n",
              "      <td>there is a forest fire at spot pond, geese are...</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>3</th>\n",
              "      <td>9</td>\n",
              "      <td>NaN</td>\n",
              "      <td>NaN</td>\n",
              "      <td>Apocalypse lighting. #Spokane #wildfires</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>4</th>\n",
              "      <td>11</td>\n",
              "      <td>NaN</td>\n",
              "      <td>NaN</td>\n",
              "      <td>Typhoon Soudelor kills 28 in China and Taiwan</td>\n",
              "    </tr>\n",
              "  </tbody>\n",
              "</table>\n",
              "</div>\n",
              "      <button class=\"colab-df-convert\" onclick=\"convertToInteractive('df-f8ced319-efb8-48fd-a908-0a300f8b2aab')\"\n",
              "              title=\"Convert this dataframe to an interactive table.\"\n",
              "              style=\"display:none;\">\n",
              "        \n",
              "  <svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\"viewBox=\"0 0 24 24\"\n",
              "       width=\"24px\">\n",
              "    <path d=\"M0 0h24v24H0V0z\" fill=\"none\"/>\n",
              "    <path d=\"M18.56 5.44l.94 2.06.94-2.06 2.06-.94-2.06-.94-.94-2.06-.94 2.06-2.06.94zm-11 1L8.5 8.5l.94-2.06 2.06-.94-2.06-.94L8.5 2.5l-.94 2.06-2.06.94zm10 10l.94 2.06.94-2.06 2.06-.94-2.06-.94-.94-2.06-.94 2.06-2.06.94z\"/><path d=\"M17.41 7.96l-1.37-1.37c-.4-.4-.92-.59-1.43-.59-.52 0-1.04.2-1.43.59L10.3 9.45l-7.72 7.72c-.78.78-.78 2.05 0 2.83L4 21.41c.39.39.9.59 1.41.59.51 0 1.02-.2 1.41-.59l7.78-7.78 2.81-2.81c.8-.78.8-2.07 0-2.86zM5.41 20L4 18.59l7.72-7.72 1.47 1.35L5.41 20z\"/>\n",
              "  </svg>\n",
              "      </button>\n",
              "      \n",
              "  <style>\n",
              "    .colab-df-container {\n",
              "      display:flex;\n",
              "      flex-wrap:wrap;\n",
              "      gap: 12px;\n",
              "    }\n",
              "\n",
              "    .colab-df-convert {\n",
              "      background-color: #E8F0FE;\n",
              "      border: none;\n",
              "      border-radius: 50%;\n",
              "      cursor: pointer;\n",
              "      display: none;\n",
              "      fill: #1967D2;\n",
              "      height: 32px;\n",
              "      padding: 0 0 0 0;\n",
              "      width: 32px;\n",
              "    }\n",
              "\n",
              "    .colab-df-convert:hover {\n",
              "      background-color: #E2EBFA;\n",
              "      box-shadow: 0px 1px 2px rgba(60, 64, 67, 0.3), 0px 1px 3px 1px rgba(60, 64, 67, 0.15);\n",
              "      fill: #174EA6;\n",
              "    }\n",
              "\n",
              "    [theme=dark] .colab-df-convert {\n",
              "      background-color: #3B4455;\n",
              "      fill: #D2E3FC;\n",
              "    }\n",
              "\n",
              "    [theme=dark] .colab-df-convert:hover {\n",
              "      background-color: #434B5C;\n",
              "      box-shadow: 0px 1px 3px 1px rgba(0, 0, 0, 0.15);\n",
              "      filter: drop-shadow(0px 1px 2px rgba(0, 0, 0, 0.3));\n",
              "      fill: #FFFFFF;\n",
              "    }\n",
              "  </style>\n",
              "\n",
              "      <script>\n",
              "        const buttonEl =\n",
              "          document.querySelector('#df-f8ced319-efb8-48fd-a908-0a300f8b2aab button.colab-df-convert');\n",
              "        buttonEl.style.display =\n",
              "          google.colab.kernel.accessAllowed ? 'block' : 'none';\n",
              "\n",
              "        async function convertToInteractive(key) {\n",
              "          const element = document.querySelector('#df-f8ced319-efb8-48fd-a908-0a300f8b2aab');\n",
              "          const dataTable =\n",
              "            await google.colab.kernel.invokeFunction('convertToInteractive',\n",
              "                                                     [key], {});\n",
              "          if (!dataTable) return;\n",
              "\n",
              "          const docLinkHtml = 'Like what you see? Visit the ' +\n",
              "            '<a target=\"_blank\" href=https://colab.research.google.com/notebooks/data_table.ipynb>data table notebook</a>'\n",
              "            + ' to learn more about interactive tables.';\n",
              "          element.innerHTML = '';\n",
              "          dataTable['output_type'] = 'display_data';\n",
              "          await google.colab.output.renderOutput(dataTable, element);\n",
              "          const docLink = document.createElement('div');\n",
              "          docLink.innerHTML = docLinkHtml;\n",
              "          element.appendChild(docLink);\n",
              "        }\n",
              "      </script>\n",
              "    </div>\n",
              "  </div>\n",
              "  "
            ]
          },
          "metadata": {},
          "execution_count": 8
        }
      ],
      "source": [
        "# The test data doesn't have a target (that's what we'd try to predict)\n",
        "test_df.head()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "O4JhBRn5Mn-V"
      },
      "source": [
        "Let's check how many examples of each target we have."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 9,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "k4P5DnLhIciD",
        "outputId": "375216c9-317b-46a3-be61-b2501ea76e48"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "0    4342\n",
              "1    3271\n",
              "Name: target, dtype: int64"
            ]
          },
          "metadata": {},
          "execution_count": 9
        }
      ],
      "source": [
        "# How many examples of each class?\n",
        "train_df.target.value_counts()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "WjEDQ297Ihy4"
      },
      "source": [
        "Since we have two target values, we're dealing with a **binary classification** problem.\n",
        "\n",
        "It's fairly balanced too, about 60% negative class (`target = 0`) and 40% positive class (`target = 1`).\n",
        "\n",
        "Where, \n",
        "\n",
        "* `1` = a real disaster Tweet\n",
        "* `0` = not a real disaster Tweet\n",
        "\n",
        "And what about the total number of samples we have?"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 10,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "jQxg7EKKIy5L",
        "outputId": "251920ac-0570-4903-fd55-febbe3c4f987"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Total training samples: 7613\n",
            "Total test samples: 3263\n",
            "Total samples: 10876\n"
          ]
        }
      ],
      "source": [
        "# How many samples total?\n",
        "print(f\"Total training samples: {len(train_df)}\")\n",
        "print(f\"Total test samples: {len(test_df)}\")\n",
        "print(f\"Total samples: {len(train_df) + len(test_df)}\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "Q1upY8-xNPWV"
      },
      "source": [
        "Alright, seems like we've got a decent amount of training and test data. If anything, we've got an abundance of testing examples, usually a split of 90/10 (90% training, 10% testing) or 80/20 is suffice.\n",
        "\n",
        "Okay, time to visualize, let's write some code to visualize random text samples.\n",
        "\n",
        "> 🤔 **Question:** Why visualize random samples? You could visualize samples in order but this could lead to only seeing a certain subset of data. Better to visualize a substantial quantity (100+) of random samples to get an idea of the different kinds of data you're working with. In machine learning, never underestimate the power of randomness."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 11,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "vH3EXknTI3bQ",
        "outputId": "f2c69a8e-3a9e-46d7-b05e-aef832742b85"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Target: 0 (not real disaster)\n",
            "Text:\n",
            "@JamesMelville Some old testimony of weapons used to promote conflicts\n",
            "Tactics - corruption &amp; infiltration of groups\n",
            "https://t.co/cyU8zxw1oH\n",
            "\n",
            "---\n",
            "\n",
            "Target: 1 (real disaster)\n",
            "Text:\n",
            "Now Trending in Nigeria: Police charge traditional ruler others with informantÛªs  murder http://t.co/93inFxzhX0\n",
            "\n",
            "---\n",
            "\n",
            "Target: 1 (real disaster)\n",
            "Text:\n",
            "REPORTED: HIT &amp; RUN-IN ROADWAY-PROPERTY DAMAGE at 15901 STATESVILLE RD\n",
            "\n",
            "---\n",
            "\n",
            "Target: 1 (real disaster)\n",
            "Text:\n",
            "ohH NO FUKURODANI DIDN'T SURVIVE THE APOCALYPSE BOKUTO FEELS HORRIBLE  my poor boy my ppor child\n",
            "\n",
            "---\n",
            "\n",
            "Target: 1 (real disaster)\n",
            "Text:\n",
            "Maryland mansion fire that killed 6 caused by damaged plug under Christmas tree report says - Into the flames: Firefighter's bravery...\n",
            "\n",
            "---\n",
            "\n"
          ]
        }
      ],
      "source": [
        "# Let's visualize some random training examples\n",
        "import random\n",
        "random_index = random.randint(0, len(train_df)-5) # create random indexes not higher than the total number of samples\n",
        "for row in train_df_shuffled[[\"text\", \"target\"]][random_index:random_index+5].itertuples():\n",
        "  _, text, target = row\n",
        "  print(f\"Target: {target}\", \"(real disaster)\" if target > 0 else \"(not real disaster)\")\n",
        "  print(f\"Text:\\n{text}\\n\")\n",
        "  print(\"---\\n\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "1FhRRewGPNS_"
      },
      "source": [
        "### Split data into training and validation sets\n",
        "\n",
        "Since the test set has no labels and we need a way to evalaute our trained models, we'll split off some of the training data and create a validation set.\n",
        "\n",
        "When our model trains (tries patterns in the Tweet samples), it'll only see data from the training set and we can see how it performs on unseen data using the validation set.\n",
        "\n",
        "We'll convert our splits from pandas Series datatypes to lists of strings (for the text) and lists of ints (for the labels) for ease of use later.\n",
        "\n",
        "To split our training dataset and create a validation dataset, we'll use Scikit-Learn's [`train_test_split()`](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html) method and dedicate 10% of the training samples to the validation set."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 12,
      "metadata": {
        "id": "7OJf31TQ-X8s"
      },
      "outputs": [],
      "source": [
        "from sklearn.model_selection import train_test_split\n",
        "\n",
        "# Use train_test_split to split training data into training and validation sets\n",
        "train_sentences, val_sentences, train_labels, val_labels = train_test_split(train_df_shuffled[\"text\"].to_numpy(),\n",
        "                                                                            train_df_shuffled[\"target\"].to_numpy(),\n",
        "                                                                            test_size=0.1, # dedicate 10% of samples to validation set\n",
        "                                                                            random_state=42) # random state for reproducibility"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 13,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "NWGOTjanBaTQ",
        "outputId": "01e3d66e-8c1e-458d-9c65-9c445063a488"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "(6851, 6851, 762, 762)"
            ]
          },
          "metadata": {},
          "execution_count": 13
        }
      ],
      "source": [
        "# Check the lengths\n",
        "len(train_sentences), len(train_labels), len(val_sentences), len(val_labels)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 14,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "VqhvQK9wBTbw",
        "outputId": "dfb2b4ae-e808-421a-eed3-531c39fcad24"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "(array(['@mogacola @zamtriossu i screamed after hitting tweet',\n",
              "        'Imagine getting flattened by Kurt Zouma',\n",
              "        '@Gurmeetramrahim #MSGDoing111WelfareWorks Green S welfare force ke appx 65000 members har time disaster victim ki help ke liye tyar hai....',\n",
              "        \"@shakjn @C7 @Magnums im shaking in fear he's gonna hack the planet\",\n",
              "        'Somehow find you and I collide http://t.co/Ee8RpOahPk',\n",
              "        '@EvaHanderek @MarleyKnysh great times until the bus driver held us hostage in the mall parking lot lmfao',\n",
              "        'destroy the free fandom honestly',\n",
              "        'Weapons stolen from National Guard Armory in New Albany still missing #Gunsense http://t.co/lKNU8902JE',\n",
              "        '@wfaaweather Pete when will the heat wave pass? Is it really going to be mid month? Frisco Boy Scouts have a canoe trip in Okla.',\n",
              "        'Patient-reported outcomes in long-term survivors of metastatic colorectal cancer - British Journal of Surgery http://t.co/5Yl4DC1Tqt'],\n",
              "       dtype=object),\n",
              " array([0, 0, 1, 0, 0, 1, 1, 0, 1, 1]))"
            ]
          },
          "metadata": {},
          "execution_count": 14
        }
      ],
      "source": [
        "# View the first 10 training sentences and their labels\n",
        "train_sentences[:10], train_labels[:10]"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "EN-houoSD-hP"
      },
      "source": [
        "## Converting text into numbers\n",
        "\n",
        "Wonderful! We've got a training set and a validation set containing Tweets and labels.\n",
        "\n",
        "Our labels are in numerical form (`0` and `1`) but our Tweets are in string form.\n",
        "\n",
        "> 🤔 **Question:** What do you think we have to do before we can use a machine learning algorithm with our text data? \n",
        "\n",
        "If you answered something along the lines of \"turn it into numbers\", you're correct. A machine learning algorithm requires its inputs to be in numerical form.\n",
        "\n",
        "In NLP, there are two main concepts for turning text into numbers:\n",
        "* **Tokenization** - A straight mapping from word or character or sub-word to a numerical value. There are three main levels of tokenization:\n",
        "  1. Using **word-level tokenization** with the sentence \"I love TensorFlow\" might result in \"I\" being `0`, \"love\" being `1` and \"TensorFlow\" being `2`. In this case, every word in a sequence considered a single **token**.\n",
        "  2. **Character-level tokenization**, such as converting the letters A-Z to values `1-26`. In this case, every character in a sequence considered a single **token**.\n",
        "  3. **Sub-word tokenization** is in between word-level and character-level tokenization. It involves breaking invidual words into smaller parts and then converting those smaller parts into numbers. For example, \"my favourite food is pineapple pizza\" might become \"my, fav, avour, rite, fo, oo, od, is, pin, ine, app, le, piz, za\". After doing this, these sub-words would then be mapped to a numerical value. In this case, every word could be considered multiple **tokens**.\n",
        "* **Embeddings** - An embedding is a representation of natural language which can be learned. Representation comes in the form of a **feature vector**. For example, the word \"dance\" could be represented by the 5-dimensional vector `[-0.8547, 0.4559, -0.3332, 0.9877, 0.1112]`. It's important to note here, the size of the feature vector is tuneable. There are two ways to use embeddings: \n",
        "  1. **Create your own embedding** - Once your text has been turned into numbers (required for an embedding), you can put them through an embedding layer (such as [`tf.keras.layers.Embedding`](https://www.tensorflow.org/api_docs/python/tf/keras/layers/Embedding)) and an embedding representation will be learned during model training.\n",
        "  2. **Reuse a pre-learned embedding** - Many pre-trained embeddings exist online. These pre-trained embeddings have often been learned on large corpuses of text (such as all of Wikipedia) and thus have a good underlying representation of natural language. You can use a pre-trained embedding to initialize your model and fine-tune it to your own specific task.\n",
        "\n",
        "![](https://raw.githubusercontent.com/mrdbourke/tensorflow-deep-learning/main/images/08-tokenization-vs-embedding.png)\n",
        "*Example of **tokenization** (straight mapping from word to number) and **embedding** (richer representation of relationships between tokens).*\n",
        "\n",
        "> 🤔 **Question:** What level of tokenzation should I use? What embedding should should I choose?\n",
        "\n",
        "It depends on your problem. You could try character-level tokenization/embeddings and word-level tokenization/embeddings and see which perform best. You might even want to try stacking them (e.g. combining the outputs of your embedding layers using [`tf.keras.layers.concatenate`](https://www.tensorflow.org/api_docs/python/tf/keras/layers/concatenate)). \n",
        "\n",
        "If you're looking for pre-trained word embeddings, [Word2vec embeddings](http://jalammar.github.io/illustrated-word2vec/), [GloVe embeddings](https://nlp.stanford.edu/projects/glove/) and many of the options available on [TensorFlow Hub](https://tfhub.dev/s?module-type=text-embedding) are great places to start.\n",
        "\n",
        "> 🔑 **Note:** Much like searching for a pre-trained computer vision model, you can search for pre-trained word embeddings to use for your problem. Try searching for something like \"use pre-trained word embeddings in TensorFlow\"."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "8UnRcM1PELHn"
      },
      "source": [
        "### Text vectorization (tokenization)\n",
        "\n",
        "Enough talking about tokenization and embeddings, let's create some.\n",
        "\n",
        "We'll practice tokenzation (mapping our words to numbers) first.\n",
        "\n",
        "To tokenize our words, we'll use the helpful preprocessing layer [`tf.keras.layers.experimental.preprocessing.TextVectorization`](https://www.tensorflow.org/api_docs/python/tf/keras/layers/experimental/preprocessing/TextVectorization).\n",
        "\n",
        "The `TextVectorization` layer takes the following parameters:\n",
        "* `max_tokens` - The maximum number of words in your vocabulary (e.g. 20000 or the number of unique words in your text), includes a value for OOV (out of vocabulary) tokens. \n",
        "* `standardize` - Method for standardizing text. Default is `\"lower_and_strip_punctuation\"` which lowers text and removes all punctuation marks.\n",
        "* `split` - How to split text, default is `\"whitespace\"` which splits on spaces.\n",
        "* `ngrams` - How many words to contain per token split, for example, `ngrams=2` splits tokens into continuous sequences of 2.\n",
        "* `output_mode` -  How to output tokens, can be `\"int\"` (integer mapping), `\"binary\"` (one-hot encoding), `\"count\"` or `\"tf-idf\"`. See documentation for more.\n",
        "* `output_sequence_length` - Length of tokenized sequence to output. For example, if `output_sequence_length=150`, all tokenized sequences will be 150 tokens long.\n",
        "* `pad_to_max_tokens` - Defaults to `False`, if `True`, the output feature axis will be padded to `max_tokens` even if the number of unique tokens in the vocabulary is less than `max_tokens`. Only valid in certain modes, see docs for more.\n",
        "\n",
        "Let's see it in action."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 15,
      "metadata": {
        "id": "PVcZk-LcNunF"
      },
      "outputs": [],
      "source": [
        "import tensorflow as tf\n",
        "from tensorflow.keras.layers import TextVectorization # after TensorFlow 2.6\n",
        "\n",
        "# Before TensorFlow 2.6\n",
        "# from tensorflow.keras.layers.experimental.preprocessing import TextVectorization \n",
        "# Note: in TensorFlow 2.6+, you no longer need \"layers.experimental.preprocessing\"\n",
        "# you can use: \"tf.keras.layers.TextVectorization\", see https://github.com/tensorflow/tensorflow/releases/tag/v2.6.0 for more\n",
        "\n",
        "# Use the default TextVectorization variables\n",
        "text_vectorizer = TextVectorization(max_tokens=None, # how many words in the vocabulary (all of the different words in your text)\n",
        "                                    standardize=\"lower_and_strip_punctuation\", # how to process text\n",
        "                                    split=\"whitespace\", # how to split tokens\n",
        "                                    ngrams=None, # create groups of n-words?\n",
        "                                    output_mode=\"int\", # how to map tokens to numbers\n",
        "                                    output_sequence_length=None) # how long should the output sequence of tokens be?\n",
        "                                    # pad_to_max_tokens=True) # Not valid if using max_tokens=None"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "u0Ej5mzKGkK8"
      },
      "source": [
        "We've initialized a `TextVectorization` object with the default settings but let's customize it a little bit for our own use case.\n",
        "\n",
        "In particular, let's set values for `max_tokens` and `output_sequence_length`.\n",
        "\n",
        "For `max_tokens` (the number of words in the vocabulary), multiples of 10,000 (`10,000`, `20,000`, `30,000`) or the exact number of unique words in your text (e.g. `32,179`) are common values.\n",
        "\n",
        "For our use case, we'll use `10,000`.\n",
        "\n",
        "And for the `output_sequence_length` we'll use the average number of tokens per Tweet in the training set. But first, we'll need to find it."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 16,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "SQ3ZCINnR56H",
        "outputId": "8c643c75-12d9-4329-8d14-ffdff2e32ecc"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "15"
            ]
          },
          "metadata": {},
          "execution_count": 16
        }
      ],
      "source": [
        "# Find average number of tokens (words) in training Tweets\n",
        "round(sum([len(i.split()) for i in train_sentences])/len(train_sentences))"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "AFGTRcw8Hv7R"
      },
      "source": [
        "Now let's create another `TextVectorization` object using our custom parameters."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 17,
      "metadata": {
        "id": "eYPcGwdbafmW"
      },
      "outputs": [],
      "source": [
        "# Setup text vectorization with custom variables\n",
        "max_vocab_length = 10000 # max number of words to have in our vocabulary\n",
        "max_length = 15 # max length our sequences will be (e.g. how many words from a Tweet does our model see?)\n",
        "\n",
        "text_vectorizer = TextVectorization(max_tokens=max_vocab_length,\n",
        "                                    output_mode=\"int\",\n",
        "                                    output_sequence_length=max_length)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "BSWycfB3H3wV"
      },
      "source": [
        "Beautiful!\n",
        "\n",
        "To map our `TextVectorization` instance `text_vectorizer` to our data, we can call the `adapt()` method on it whilst passing it our training text."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 18,
      "metadata": {
        "id": "0083KHXPO4m2"
      },
      "outputs": [],
      "source": [
        "# Fit the text vectorizer to the training text\n",
        "text_vectorizer.adapt(train_sentences)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "Syh0VB9wIHUq"
      },
      "source": [
        "Training data mapped! Let's try our `text_vectorizer` on a custom sentence (one similar to what you might see in the training data)."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 19,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "uizmdJKvO2OW",
        "outputId": "98a93609-ff3c-4ca7-bc3f-1fba622c66d5"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "<tf.Tensor: shape=(1, 15), dtype=int64, numpy=\n",
              "array([[264,   3, 232,   4,  13, 698,   0,   0,   0,   0,   0,   0,   0,\n",
              "          0,   0]])>"
            ]
          },
          "metadata": {},
          "execution_count": 19
        }
      ],
      "source": [
        "# Create sample sentence and tokenize it\n",
        "sample_sentence = \"There's a flood in my street!\"\n",
        "text_vectorizer([sample_sentence])"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "M0RmAeplIW57"
      },
      "source": [
        "Wonderful, it seems we've got a way to turn our text into numbers (in this case, word-level tokenization). Notice the 0's at the end of the returned tensor, this is because we set `output_sequence_length=15`, meaning no matter the size of the sequence we pass to `text_vectorizer`, it always returns a sequence with a length of 15.\n",
        "\n",
        "How about we try our `text_vectorizer` on a few random sentences?"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 20,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "SZFka4BtRR6_",
        "outputId": "63d5a3b0-7e9f-4660-f1a4-1c07043cd3dc"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Original text:\n",
            "new summer long thin body bag hip A word skirt Blue http://t.co/lvKoEMsq8m http://t.co/CjiRhHh4vj      \n",
            "\n",
            "Vectorized version:\n"
          ]
        },
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "<tf.Tensor: shape=(1, 15), dtype=int64, numpy=\n",
              "array([[  50,  270,  480, 3335,   83,  322, 2436,    3, 1448, 3407,  824,\n",
              "           1,    1,    0,    0]])>"
            ]
          },
          "metadata": {},
          "execution_count": 20
        }
      ],
      "source": [
        "# Choose a random sentence from the training dataset and tokenize it\n",
        "random_sentence = random.choice(train_sentences)\n",
        "print(f\"Original text:\\n{random_sentence}\\\n",
        "      \\n\\nVectorized version:\")\n",
        "text_vectorizer([random_sentence])"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "PErGKRbPJF89"
      },
      "source": [
        "Looking good!\n",
        "\n",
        "Finally, we can check the unique tokens in our vocabulary using the `get_vocabulary()` method."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 21,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "5nwNdgAZIhna",
        "outputId": "a9e03ebb-4a87-4bef-d224-c0dcaed96181"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Number of words in vocab: 10000\n",
            "Top 5 most common words: ['', '[UNK]', 'the', 'a', 'in']\n",
            "Bottom 5 least common words: ['pages', 'paeds', 'pads', 'padres', 'paddytomlinson1']\n"
          ]
        }
      ],
      "source": [
        "# Get the unique words in the vocabulary\n",
        "words_in_vocab = text_vectorizer.get_vocabulary()\n",
        "top_5_words = words_in_vocab[:5] # most common tokens (notice the [UNK] token for \"unknown\" words)\n",
        "bottom_5_words = words_in_vocab[-5:] # least common tokens\n",
        "print(f\"Number of words in vocab: {len(words_in_vocab)}\")\n",
        "print(f\"Top 5 most common words: {top_5_words}\") \n",
        "print(f\"Bottom 5 least common words: {bottom_5_words}\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "AHyCdO0uEOkH"
      },
      "source": [
        "### Creating an Embedding using an Embedding Layer\n",
        "\n",
        "We've got a way to map our text to numbers. How about we go a step further and turn those numbers into an embedding?\n",
        "\n",
        "The powerful thing about an embedding is it can be learned during training. This means rather than just being static (e.g. `1` = I, `2` = love, `3` = TensorFlow), a word's numeric representation can be improved as a model goes through data samples.\n",
        "\n",
        "We can see what an embedding of a word looks like by using the [`tf.keras.layers.Embedding`](https://www.tensorflow.org/api_docs/python/tf/keras/layers/Embedding) layer. \n",
        "\n",
        "The main parameters we're concerned about here are:\n",
        "* `input_dim` - The size of the vocabulary (e.g. `len(text_vectorizer.get_vocabulary()`).\n",
        "* `output_dim` - The size of the output embedding vector, for example, a value of `100` outputs a  feature vector of size 100 for each word.\n",
        "* `embeddings_initializer` - How to initialize the embeddings matrix, default is `\"uniform\"` which randomly initalizes embedding matrix with uniform distribution. This can be changed for using pre-learned embeddings.\n",
        "* `input_length` - Length of sequences being passed to embedding layer.\n",
        "\n",
        "Knowing these, let's make an embedding layer."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 22,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "OsB4StymSk_s",
        "outputId": "ae557704-a630-41ce-b0e7-ba6558165aff"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "<keras.layers.core.embedding.Embedding at 0x7f0c118dcc40>"
            ]
          },
          "metadata": {},
          "execution_count": 22
        }
      ],
      "source": [
        "tf.random.set_seed(42)\n",
        "from tensorflow.keras import layers\n",
        "\n",
        "embedding = layers.Embedding(input_dim=max_vocab_length, # set input shape\n",
        "                             output_dim=128, # set size of embedding vector\n",
        "                             embeddings_initializer=\"uniform\", # default, intialize randomly\n",
        "                             input_length=max_length, # how long is each input\n",
        "                             name=\"embedding_1\") \n",
        "\n",
        "embedding"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "bfML_IzlSUho"
      },
      "source": [
        "Excellent, notice how `embedding` is a TensoFlow layer? This is important because we can use it as part of a model, meaning its parameters (word representations) can be updated and improved as the model learns.\n",
        "\n",
        "How about we try it out on a sample sentence?"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 23,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "1Re6Eew6SZnG",
        "outputId": "e7f62bb5-49d0-4f77-8c2c-bff0c941ba94"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Original text:\n",
            "Now on #ComDev #Asia: Radio stations in #Bangladesh broadcasting #programs ?to address the upcoming cyclone #komen http://t.co/iOVr4yMLKp      \n",
            "\n",
            "Embedded version:\n"
          ]
        },
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "<tf.Tensor: shape=(1, 15, 128), dtype=float32, numpy=\n",
              "array([[[-0.04868475, -0.03902867, -0.01375594, ...,  0.01682534,\n",
              "         -0.0439401 , -0.04604518],\n",
              "        [-0.04827927, -0.00328457,  0.02171678, ..., -0.03261749,\n",
              "         -0.01061803, -0.0481179 ],\n",
              "        [-0.02431345,  0.01104342,  0.00933889, ..., -0.04607272,\n",
              "         -0.00651377,  0.03853123],\n",
              "        ...,\n",
              "        [-0.03270339,  0.03608486,  0.03573406, ...,  0.03622421,\n",
              "          0.03427652, -0.03483479],\n",
              "        [-0.0489977 ,  0.01962234,  0.02186165, ...,  0.03139002,\n",
              "         -0.00744159,  0.0428594 ],\n",
              "        [ 0.01265842,  0.02462569, -0.04731182, ...,  0.00403734,\n",
              "          0.0431679 ,  0.03959754]]], dtype=float32)>"
            ]
          },
          "metadata": {},
          "execution_count": 23
        }
      ],
      "source": [
        "# Get a random sentence from training set\n",
        "random_sentence = random.choice(train_sentences)\n",
        "print(f\"Original text:\\n{random_sentence}\\\n",
        "      \\n\\nEmbedded version:\")\n",
        "\n",
        "# Embed the random sentence (turn it into numerical representation)\n",
        "sample_embed = embedding(text_vectorizer([random_sentence]))\n",
        "sample_embed"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "e4Sn8o9pTBE5"
      },
      "source": [
        "Each token in the sentence gets turned into a length 128 feature vector."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 24,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "g_VBepuSTBDW",
        "outputId": "f04fe6bf-bcac-4dd2-eff7-e9ed370e26c2"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "<tf.Tensor: shape=(128,), dtype=float32, numpy=\n",
              "array([-0.04868475, -0.03902867, -0.01375594,  0.01587117, -0.02964617,\n",
              "        0.00738639, -0.03109504,  0.03008839, -0.01458266, -0.03069887,\n",
              "       -0.04926676, -0.03454053, -0.04019499, -0.04406711,  0.01975099,\n",
              "        0.02852687, -0.04052209, -0.03800124,  0.03438697, -0.0118026 ,\n",
              "       -0.03470664, -0.01146972,  0.0449667 , -0.00269016,  0.02131964,\n",
              "       -0.04141569, -0.03724197,  0.01624352,  0.03269556,  0.03813741,\n",
              "        0.03606123,  0.00698509, -0.03569689,  0.02056131, -0.03467314,\n",
              "        0.01110398,  0.02095172,  0.02219674, -0.04576088, -0.04229112,\n",
              "       -0.02345047,  0.02578488,  0.02985479, -0.00203061,  0.03920727,\n",
              "        0.04065951,  0.03973453,  0.03947322,  0.01699554,  0.0021927 ,\n",
              "        0.03676197, -0.04327145,  0.02495482,  0.02447238, -0.04413594,\n",
              "       -0.01388069, -0.00375951, -0.0328602 , -0.00067427,  0.01808068,\n",
              "        0.04227355,  0.02817165,  0.01965401, -0.01514393,  0.01905935,\n",
              "       -0.03820103, -0.04916845,  0.02303007,  0.00830983,  0.01011454,\n",
              "       -0.04043181,  0.02080727, -0.03319015,  0.04188809, -0.01183917,\n",
              "       -0.01822531, -0.02172413,  0.03059311,  0.02727925, -0.00328885,\n",
              "       -0.00808424, -0.02095444, -0.00894216,  0.00770078, -0.00439024,\n",
              "        0.03637768,  0.02007255, -0.02650907, -0.01374531,  0.01806785,\n",
              "       -0.03309877, -0.01076321, -0.04107616,  0.01709371,  0.04567242,\n",
              "       -0.01824218,  0.02805582,  0.02974418, -0.04001283, -0.04077357,\n",
              "        0.00323737,  0.04038842, -0.00992844, -0.03974843,  0.04533138,\n",
              "        0.04738795,  0.02837384,  0.03874009, -0.01673441, -0.00258055,\n",
              "       -0.01975214, -0.04166807, -0.02483889, -0.02804886,  0.04608755,\n",
              "        0.03544754,  0.02697959,  0.00242041,  0.00101637, -0.01162767,\n",
              "       -0.00497937,  0.00540714, -0.01258825,  0.00779672,  0.02742722,\n",
              "        0.01682534, -0.0439401 , -0.04604518], dtype=float32)>"
            ]
          },
          "metadata": {},
          "execution_count": 24
        }
      ],
      "source": [
        "# Check out a single token's embedding\n",
        "sample_embed[0][0]"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "Z0NTsDklR0xw"
      },
      "source": [
        "These values might not mean much to us but they're what our computer sees each word as. When our model looks for patterns in different samples, these values will be updated as necessary.\n",
        "\n",
        "> 🔑 **Note:** The previous two concepts (tokenization and embeddings) are the foundation for many NLP tasks. So if you're not sure about anything, be sure to research and conduct your own experiments to further help your understanding."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "ZJENUdF3F7Rn"
      },
      "source": [
        "## Modelling a text dataset\n",
        "\n",
        "![](https://raw.githubusercontent.com/mrdbourke/tensorflow-deep-learning/main/images/08-inputs-and-outputs-with-shapes-and-models-were-going-to-build.png)\n",
        "*Once you've got your inputs and outputs prepared, it's a matter of figuring out which machine learning model to build in between them to bridge the gap.*\n",
        "\n",
        "Now that we've got a way to turn our text data into numbers, we can start to build machine learning models to model it.\n",
        "\n",
        "To get plenty of practice, we're going to build a series of different models, each as its own experiment. We'll then compare the results of each model and see which one performed best.\n",
        "\n",
        "More specifically, we'll be building the following:\n",
        "* **Model 0**: Naive Bayes (baseline)\n",
        "* **Model 1**: Feed-forward neural network (dense model)\n",
        "* **Model 2**: LSTM model\n",
        "* **Model 3**: GRU model\n",
        "* **Model 4**: Bidirectional-LSTM model\n",
        "* **Model 5**: 1D Convolutional Neural Network\n",
        "* **Model 6**: TensorFlow Hub Pretrained Feature Extractor\n",
        "* **Model 7**: Same as model 6 with 10% of training data\n",
        "\n",
        "Model 0 is the simplest to acquire a baseline which we'll expect each other of the other deeper models to beat.\n",
        "\n",
        "Each experiment will go through the following steps:\n",
        "* Construct the model\n",
        "* Train the model\n",
        "* Make predictions with the model\n",
        "* Track prediction evaluation metrics for later comparison\n",
        "\n",
        "Let's get started."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "q4i5BiQfF--y"
      },
      "source": [
        "### Model 0: Getting a baseline\n",
        "\n",
        "As with all machine learning modelling experiments, it's important to create a baseline model so you've got a benchmark for future experiments to build upon.\n",
        "\n",
        "To create our baseline, we'll create a Scikit-Learn Pipeline using the TF-IDF (term frequency-inverse document frequency) formula to convert our words to numbers and then model them with the [Multinomial Naive Bayes algorithm](https://scikit-learn.org/stable/modules/generated/sklearn.naive_bayes.MultinomialNB.html#sklearn.naive_bayes.MultinomialNB). This was chosen via referring to the [Scikit-Learn machine learning map](https://scikit-learn.org/stable/tutorial/machine_learning_map/index.html).\n",
        "\n",
        "> 📖 **Reading:** The ins and outs of TF-IDF algorithm is beyond the scope of this notebook, however, the curious reader is encouraged to check out the [Scikit-Learn documentation for more](https://scikit-learn.org/stable/modules/feature_extraction.html#tfidf-term-weighting)."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 25,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 126
        },
        "id": "xFqjqWcXtOOs",
        "outputId": "9f5e6d10-7fe1-441b-fac9-c3e0a45e6a3e"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "Pipeline(steps=[('tfidf', TfidfVectorizer()), ('clf', MultinomialNB())])"
            ],
            "text/html": [
              "<style>#sk-container-id-1 {color: black;background-color: white;}#sk-container-id-1 pre{padding: 0;}#sk-container-id-1 div.sk-toggleable {background-color: white;}#sk-container-id-1 label.sk-toggleable__label {cursor: pointer;display: block;width: 100%;margin-bottom: 0;padding: 0.3em;box-sizing: border-box;text-align: center;}#sk-container-id-1 label.sk-toggleable__label-arrow:before {content: \"▸\";float: left;margin-right: 0.25em;color: #696969;}#sk-container-id-1 label.sk-toggleable__label-arrow:hover:before {color: black;}#sk-container-id-1 div.sk-estimator:hover label.sk-toggleable__label-arrow:before {color: black;}#sk-container-id-1 div.sk-toggleable__content {max-height: 0;max-width: 0;overflow: hidden;text-align: left;background-color: #f0f8ff;}#sk-container-id-1 div.sk-toggleable__content pre {margin: 0.2em;color: black;border-radius: 0.25em;background-color: #f0f8ff;}#sk-container-id-1 input.sk-toggleable__control:checked~div.sk-toggleable__content {max-height: 200px;max-width: 100%;overflow: auto;}#sk-container-id-1 input.sk-toggleable__control:checked~label.sk-toggleable__label-arrow:before {content: \"▾\";}#sk-container-id-1 div.sk-estimator input.sk-toggleable__control:checked~label.sk-toggleable__label {background-color: #d4ebff;}#sk-container-id-1 div.sk-label input.sk-toggleable__control:checked~label.sk-toggleable__label {background-color: #d4ebff;}#sk-container-id-1 input.sk-hidden--visually {border: 0;clip: rect(1px 1px 1px 1px);clip: rect(1px, 1px, 1px, 1px);height: 1px;margin: -1px;overflow: hidden;padding: 0;position: absolute;width: 1px;}#sk-container-id-1 div.sk-estimator {font-family: monospace;background-color: #f0f8ff;border: 1px dotted black;border-radius: 0.25em;box-sizing: border-box;margin-bottom: 0.5em;}#sk-container-id-1 div.sk-estimator:hover {background-color: #d4ebff;}#sk-container-id-1 div.sk-parallel-item::after {content: \"\";width: 100%;border-bottom: 1px solid gray;flex-grow: 1;}#sk-container-id-1 div.sk-label:hover label.sk-toggleable__label {background-color: #d4ebff;}#sk-container-id-1 div.sk-serial::before {content: \"\";position: absolute;border-left: 1px solid gray;box-sizing: border-box;top: 0;bottom: 0;left: 50%;z-index: 0;}#sk-container-id-1 div.sk-serial {display: flex;flex-direction: column;align-items: center;background-color: white;padding-right: 0.2em;padding-left: 0.2em;position: relative;}#sk-container-id-1 div.sk-item {position: relative;z-index: 1;}#sk-container-id-1 div.sk-parallel {display: flex;align-items: stretch;justify-content: center;background-color: white;position: relative;}#sk-container-id-1 div.sk-item::before, #sk-container-id-1 div.sk-parallel-item::before {content: \"\";position: absolute;border-left: 1px solid gray;box-sizing: border-box;top: 0;bottom: 0;left: 50%;z-index: -1;}#sk-container-id-1 div.sk-parallel-item {display: flex;flex-direction: column;z-index: 1;position: relative;background-color: white;}#sk-container-id-1 div.sk-parallel-item:first-child::after {align-self: flex-end;width: 50%;}#sk-container-id-1 div.sk-parallel-item:last-child::after {align-self: flex-start;width: 50%;}#sk-container-id-1 div.sk-parallel-item:only-child::after {width: 0;}#sk-container-id-1 div.sk-dashed-wrapped {border: 1px dashed gray;margin: 0 0.4em 0.5em 0.4em;box-sizing: border-box;padding-bottom: 0.4em;background-color: white;}#sk-container-id-1 div.sk-label label {font-family: monospace;font-weight: bold;display: inline-block;line-height: 1.2em;}#sk-container-id-1 div.sk-label-container {text-align: center;}#sk-container-id-1 div.sk-container {/* jupyter's `normalize.less` sets `[hidden] { display: none; }` but bootstrap.min.css set `[hidden] { display: none !important; }` so we also need the `!important` here to be able to override the default hidden behavior on the sphinx rendered scikit-learn.org. See: https://github.com/scikit-learn/scikit-learn/issues/21755 */display: inline-block !important;position: relative;}#sk-container-id-1 div.sk-text-repr-fallback {display: none;}</style><div id=\"sk-container-id-1\" class=\"sk-top-container\"><div class=\"sk-text-repr-fallback\"><pre>Pipeline(steps=[(&#x27;tfidf&#x27;, TfidfVectorizer()), (&#x27;clf&#x27;, MultinomialNB())])</pre><b>In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. <br />On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.</b></div><div class=\"sk-container\" hidden><div class=\"sk-item sk-dashed-wrapped\"><div class=\"sk-label-container\"><div class=\"sk-label sk-toggleable\"><input class=\"sk-toggleable__control sk-hidden--visually\" id=\"sk-estimator-id-1\" type=\"checkbox\" ><label for=\"sk-estimator-id-1\" class=\"sk-toggleable__label sk-toggleable__label-arrow\">Pipeline</label><div class=\"sk-toggleable__content\"><pre>Pipeline(steps=[(&#x27;tfidf&#x27;, TfidfVectorizer()), (&#x27;clf&#x27;, MultinomialNB())])</pre></div></div></div><div class=\"sk-serial\"><div class=\"sk-item\"><div class=\"sk-estimator sk-toggleable\"><input class=\"sk-toggleable__control sk-hidden--visually\" id=\"sk-estimator-id-2\" type=\"checkbox\" ><label for=\"sk-estimator-id-2\" class=\"sk-toggleable__label sk-toggleable__label-arrow\">TfidfVectorizer</label><div class=\"sk-toggleable__content\"><pre>TfidfVectorizer()</pre></div></div></div><div class=\"sk-item\"><div class=\"sk-estimator sk-toggleable\"><input class=\"sk-toggleable__control sk-hidden--visually\" id=\"sk-estimator-id-3\" type=\"checkbox\" ><label for=\"sk-estimator-id-3\" class=\"sk-toggleable__label sk-toggleable__label-arrow\">MultinomialNB</label><div class=\"sk-toggleable__content\"><pre>MultinomialNB()</pre></div></div></div></div></div></div></div>"
            ]
          },
          "metadata": {},
          "execution_count": 25
        }
      ],
      "source": [
        "from sklearn.feature_extraction.text import TfidfVectorizer\n",
        "from sklearn.naive_bayes import MultinomialNB\n",
        "from sklearn.pipeline import Pipeline\n",
        "\n",
        "# Create tokenization and modelling pipeline\n",
        "model_0 = Pipeline([\n",
        "                    (\"tfidf\", TfidfVectorizer()), # convert words to numbers using tfidf\n",
        "                    (\"clf\", MultinomialNB()) # model the text\n",
        "])\n",
        "\n",
        "# Fit the pipeline to the training data\n",
        "model_0.fit(train_sentences, train_labels)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "ybOvOuVJbNjg"
      },
      "source": [
        "The benefit of using a shallow model like Multinomial Naive Bayes is that training is very fast.\n",
        "\n",
        "Let's evaluate our model and find our baseline metric."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 26,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "soPfnpmQuUIP",
        "outputId": "402cf204-0d0f-4cd0-d647-b2e3e260c72f"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Our baseline model achieves an accuracy of: 79.27%\n"
          ]
        }
      ],
      "source": [
        "baseline_score = model_0.score(val_sentences, val_labels)\n",
        "print(f\"Our baseline model achieves an accuracy of: {baseline_score*100:.2f}%\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "hUv5dyuibf3M"
      },
      "source": [
        "How about we make some predictions with our baseline model?"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 27,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "7n89JxrJufcf",
        "outputId": "7f0222e7-6229-442e-8615-7cc4ac483e6d"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "array([1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1])"
            ]
          },
          "metadata": {},
          "execution_count": 27
        }
      ],
      "source": [
        "# Make predictions\n",
        "baseline_preds = model_0.predict(val_sentences)\n",
        "baseline_preds[:20]"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "K354svk_bmdf"
      },
      "source": [
        "### Creating an evaluation function for our model experiments\n",
        "\n",
        "We could evaluate these as they are but since we're going to be evaluating several models in the same way going forward, let's create a helper function which takes an array of predictions and ground truth labels and computes the following:\n",
        "* Accuracy\n",
        "* Precision\n",
        "* Recall\n",
        "* F1-score\n",
        "\n",
        "> 🔑 **Note:** Since we're dealing with a classification problem, the above metrics are the most appropriate. If we were working with a regression problem, other metrics such as MAE (mean absolute error) would be a better choice."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 28,
      "metadata": {
        "id": "gLmNlDjIxGgJ"
      },
      "outputs": [],
      "source": [
        "# Function to evaluate: accuracy, precision, recall, f1-score\n",
        "from sklearn.metrics import accuracy_score, precision_recall_fscore_support\n",
        "\n",
        "def calculate_results(y_true, y_pred):\n",
        "  \"\"\"\n",
        "  Calculates model accuracy, precision, recall and f1 score of a binary classification model.\n",
        "\n",
        "  Args:\n",
        "  -----\n",
        "  y_true = true labels in the form of a 1D array\n",
        "  y_pred = predicted labels in the form of a 1D array\n",
        "\n",
        "  Returns a dictionary of accuracy, precision, recall, f1-score.\n",
        "  \"\"\"\n",
        "  # Calculate model accuracy\n",
        "  model_accuracy = accuracy_score(y_true, y_pred) * 100\n",
        "  # Calculate model precision, recall and f1 score using \"weighted\" average\n",
        "  model_precision, model_recall, model_f1, _ = precision_recall_fscore_support(y_true, y_pred, average=\"weighted\")\n",
        "  model_results = {\"accuracy\": model_accuracy,\n",
        "                  \"precision\": model_precision,\n",
        "                  \"recall\": model_recall,\n",
        "                  \"f1\": model_f1}\n",
        "  return model_results"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 29,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "Sgy1omMhwr52",
        "outputId": "15bd29d7-97ca-46f4-9edc-ad8d8d7f543e"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "{'accuracy': 79.26509186351706,\n",
              " 'precision': 0.8111390004213173,\n",
              " 'recall': 0.7926509186351706,\n",
              " 'f1': 0.7862189758049549}"
            ]
          },
          "metadata": {},
          "execution_count": 29
        }
      ],
      "source": [
        "# Get baseline results\n",
        "baseline_results = calculate_results(y_true=val_labels,\n",
        "                                     y_pred=baseline_preds)\n",
        "baseline_results"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "noRJNm7dGNyh"
      },
      "source": [
        "### Model 1: A simple dense model\n",
        "\n",
        "The first \"deep\" model we're going to build is a single layer dense model. In fact, it's barely going to have a single layer. \n",
        "\n",
        "It'll take our text and labels as input, tokenize the text, create an embedding, find the average of the embedding (using Global Average Pooling) and then pass the average through a fully connected layer with one output unit and a sigmoid activation function.\n",
        "\n",
        "If the previous sentence sounds like a mouthful, it'll make sense when we code it out (remember, if in doubt, code it out).\n",
        "\n",
        "And since we're going to be building a number of TensorFlow deep learning models, we'll import our `create_tensorboard_callback()` function from `helper_functions.py` to keep track of the results of each. "
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 30,
      "metadata": {
        "id": "PVMPUd3HTit5"
      },
      "outputs": [],
      "source": [
        "# Create tensorboard callback (need to create a new one for each model)\n",
        "from helper_functions import create_tensorboard_callback\n",
        "\n",
        "# Create directory to save TensorBoard logs\n",
        "SAVE_DIR = \"model_logs\""
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "Pib8hHtu7vt1"
      },
      "source": [
        "Now we've got a TensorBoard callback function ready to go, let's build our first deep model."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 31,
      "metadata": {
        "id": "a_rVtJA7yVBI"
      },
      "outputs": [],
      "source": [
        "# Build model with the Functional API\n",
        "from tensorflow.keras import layers\n",
        "inputs = layers.Input(shape=(1,), dtype=\"string\") # inputs are 1-dimensional strings\n",
        "x = text_vectorizer(inputs) # turn the input text into numbers\n",
        "x = embedding(x) # create an embedding of the numerized numbers\n",
        "x = layers.GlobalAveragePooling1D()(x) # lower the dimensionality of the embedding (try running the model without this layer and see what happens)\n",
        "outputs = layers.Dense(1, activation=\"sigmoid\")(x) # create the output layer, want binary outputs so use sigmoid activation\n",
        "model_1 = tf.keras.Model(inputs, outputs, name=\"model_1_dense\") # construct the model"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "JYzsu36Y8JUe"
      },
      "source": [
        "Looking good. Our model takes a 1-dimensional string as input (in our case, a Tweet), it then tokenizes the string using `text_vectorizer` and creates an embedding using `embedding`.\n",
        "\n",
        "We then (optionally) pool the outputs of the embedding layer to reduce the dimensionality of the tensor we pass to the output layer.\n",
        "\n",
        "> 🛠 **Exercise:** Try building `model_1` with and without a `GlobalAveragePooling1D()` layer after the `embedding` layer. What happens? Why do you think this is?\n",
        "\n",
        "Finally, we pass the output of the pooling layer to a dense layer with sigmoid activation (we use sigmoid since our problem is binary classification).\n",
        "\n",
        "Before we can fit our model to the data, we've got to compile it. Since we're working with binary classification, we'll use `\"binary_crossentropy\"` as our loss function and the Adam optimizer."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 32,
      "metadata": {
        "id": "Ubq0ctLD8CQq"
      },
      "outputs": [],
      "source": [
        "# Compile model\n",
        "model_1.compile(loss=\"binary_crossentropy\",\n",
        "                optimizer=tf.keras.optimizers.Adam(),\n",
        "                metrics=[\"accuracy\"])"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "crgltz1O9uku"
      },
      "source": [
        "Model compiled. Let's get a summary."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 33,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "QkJa-t8aTw1H",
        "outputId": "78485624-0c4e-4ab9-ab61-e5fbe7cff576"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Model: \"model_1_dense\"\n",
            "_________________________________________________________________\n",
            " Layer (type)                Output Shape              Param #   \n",
            "=================================================================\n",
            " input_1 (InputLayer)        [(None, 1)]               0         \n",
            "                                                                 \n",
            " text_vectorization_1 (TextV  (None, 15)               0         \n",
            " ectorization)                                                   \n",
            "                                                                 \n",
            " embedding_1 (Embedding)     (None, 15, 128)           1280000   \n",
            "                                                                 \n",
            " global_average_pooling1d (G  (None, 128)              0         \n",
            " lobalAveragePooling1D)                                          \n",
            "                                                                 \n",
            " dense (Dense)               (None, 1)                 129       \n",
            "                                                                 \n",
            "=================================================================\n",
            "Total params: 1,280,129\n",
            "Trainable params: 1,280,129\n",
            "Non-trainable params: 0\n",
            "_________________________________________________________________\n"
          ]
        }
      ],
      "source": [
        "# Get a summary of the model\n",
        "model_1.summary()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "bH0JLyR09yYt"
      },
      "source": [
        "Most of the trainable parameters are contained within the embedding layer. Recall we created an embedding of size 128 (`output_dim=128`) for a vocabulary of size 10,000 (`input_dim=10000`), hence the 1,280,000 trainable parameters.\n",
        "\n",
        "Alright, our model is compiled, let's fit it to our training data for 5 epochs. We'll also pass our TensorBoard callback function to make sure our model's training metrics are logged."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 34,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "1YRYpJIfTvHV",
        "outputId": "1f3854ae-056b-4bd0-8894-e8d9ec3df3cd"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Saving TensorBoard log files to: model_logs/simple_dense_model/20230526-001451\n",
            "Epoch 1/5\n",
            "215/215 [==============================] - 18s 55ms/step - loss: 0.6098 - accuracy: 0.6936 - val_loss: 0.5360 - val_accuracy: 0.7559\n",
            "Epoch 2/5\n",
            "215/215 [==============================] - 2s 11ms/step - loss: 0.4417 - accuracy: 0.8194 - val_loss: 0.4691 - val_accuracy: 0.7887\n",
            "Epoch 3/5\n",
            "215/215 [==============================] - 2s 10ms/step - loss: 0.3471 - accuracy: 0.8616 - val_loss: 0.4588 - val_accuracy: 0.7887\n",
            "Epoch 4/5\n",
            "215/215 [==============================] - 2s 7ms/step - loss: 0.2856 - accuracy: 0.8921 - val_loss: 0.4637 - val_accuracy: 0.7913\n",
            "Epoch 5/5\n",
            "215/215 [==============================] - 2s 8ms/step - loss: 0.2388 - accuracy: 0.9115 - val_loss: 0.4760 - val_accuracy: 0.7861\n"
          ]
        }
      ],
      "source": [
        "# Fit the model\n",
        "model_1_history = model_1.fit(train_sentences, # input sentences can be a list of strings due to text preprocessing layer built-in model\n",
        "                              train_labels,\n",
        "                              epochs=5,\n",
        "                              validation_data=(val_sentences, val_labels),\n",
        "                              callbacks=[create_tensorboard_callback(dir_name=SAVE_DIR, \n",
        "                                                                     experiment_name=\"simple_dense_model\")])"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "kZR5_j9C_LW-"
      },
      "source": [
        "Nice! Since we're using such a simple model, each epoch processes very quickly.\n",
        "\n",
        "Let's check our model's performance on the validation set."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 35,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "zSTS87YGzuBG",
        "outputId": "416c8d9a-5f01-4c3c-e8e1-a0ceb57d97a0"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "24/24 [==============================] - 0s 2ms/step - loss: 0.4760 - accuracy: 0.7861\n"
          ]
        },
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "[0.4760194718837738, 0.7860892415046692]"
            ]
          },
          "metadata": {},
          "execution_count": 35
        }
      ],
      "source": [
        "# Check the results\n",
        "model_1.evaluate(val_sentences, val_labels)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 36,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "5M2CTAetBVfW",
        "outputId": "e539ac4f-33ed-40be-9921-30378ad8b3c2"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "[<tf.Variable 'embedding_1/embeddings:0' shape=(10000, 128) dtype=float32, numpy=\n",
              " array([[-0.01078545,  0.05590528,  0.03125916, ..., -0.0312557 ,\n",
              "         -0.05340781, -0.03800201],\n",
              "        [-0.02370532,  0.01161508,  0.0097667 , ..., -0.04962142,\n",
              "         -0.00636482,  0.03781125],\n",
              "        [-0.05472897,  0.05356752,  0.02146765, ...,  0.05501205,\n",
              "          0.01705659, -0.05321405],\n",
              "        ...,\n",
              "        [ 0.01756669, -0.03676652, -0.00949616, ..., -0.00987446,\n",
              "         -0.04183743,  0.03016822],\n",
              "        [-0.07823883,  0.06081628, -0.07657789, ...,  0.07998865,\n",
              "         -0.05281445, -0.02332675],\n",
              "        [-0.03393482,  0.08871375, -0.06819566, ...,  0.06992952,\n",
              "         -0.09992232, -0.02705033]], dtype=float32)>]"
            ]
          },
          "metadata": {},
          "execution_count": 36
        }
      ],
      "source": [
        "embedding.weights"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 37,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "M3rfhJFSBrga",
        "outputId": "442907dd-0889-421f-8846-b312c107b53f"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "(10000, 128)\n"
          ]
        }
      ],
      "source": [
        "embed_weights = model_1.get_layer(\"embedding_1\").get_weights()[0]\n",
        "print(embed_weights.shape)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "I9dg2aba_VxK"
      },
      "source": [
        "And since we tracked our model's training logs with TensorBoard, how about we visualize them?\n",
        "\n",
        "We can do so by uploading our TensorBoard log files (contained in the `model_logs` directory) to [TensorBoard.dev](https://tensorboard.dev/).\n",
        "\n",
        "> 🔑 **Note:** Remember, whatever you upload to TensorBoard.dev becomes public. If there are training logs you don't want to share, don't upload them."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 38,
      "metadata": {
        "id": "t6UrSgRVU6pl"
      },
      "outputs": [],
      "source": [
        "# # View tensorboard logs of transfer learning modelling experiments (should be 4 models)\n",
        "# # Upload TensorBoard dev records\n",
        "# !tensorboard dev upload --logdir ./model_logs \\\n",
        "#   --name \"First deep model on text data\" \\\n",
        "#   --description \"Trying a dense model with an embedding layer\" \\\n",
        "#   --one_shot # exits the uploader when upload has finished"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 39,
      "metadata": {
        "id": "DVyJl-VE1ACz"
      },
      "outputs": [],
      "source": [
        "# If you need to remove previous experiments, you can do so using the following command\n",
        "# !tensorboard dev delete --experiment_id EXPERIMENT_ID_TO_DELETE"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "PkinGcjQ_yI9"
      },
      "source": [
        "The TensorBoard.dev experiment for our first deep model can be viewed here: https://tensorboard.dev/experiment/5d1Xm10aT6m6MgyW3HAGfw/\n",
        "\n",
        "![](https://raw.githubusercontent.com/mrdbourke/tensorflow-deep-learning/main/images/08-tensorboard-dense-model-training-curves.png)\n",
        "\n",
        "*What the training curves of our model look like on TensorBoard. From looking at the curves can you tell if the model is overfitting or underfitting?*\n",
        "\n",
        "Beautiful! Those are some colorful training curves. Would you say the model is overfitting or underfitting?\n",
        "\n",
        "We've built and trained our first deep model, the next step is to make some predictions with it."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 40,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "5X7kbEmAzzxM",
        "outputId": "7433ca4a-f851-41b6-9d20-2cc45dc08bdb"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "24/24 [==============================] - 0s 2ms/step\n"
          ]
        },
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "array([[0.4068562 ],\n",
              "       [0.74714756],\n",
              "       [0.9978309 ],\n",
              "       [0.10913013],\n",
              "       [0.10925023],\n",
              "       [0.93645686],\n",
              "       [0.91428435],\n",
              "       [0.99250424],\n",
              "       [0.96829313],\n",
              "       [0.26842445]], dtype=float32)"
            ]
          },
          "metadata": {},
          "execution_count": 40
        }
      ],
      "source": [
        "# Make predictions (these come back in the form of probabilities)\n",
        "model_1_pred_probs = model_1.predict(val_sentences)\n",
        "model_1_pred_probs[:10] # only print out the first 10 prediction probabilities"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "YWU5e1NLAKJ9"
      },
      "source": [
        "Since our final layer uses a sigmoid activation function, we get our predictions back in the form of probabilities.\n",
        "\n",
        "To convert them to prediction classes, we'll use `tf.round()`, meaning prediction probabilities below 0.5 will be rounded to 0 and those above 0.5 will be rounded to 1.\n",
        "\n",
        "> 🔑 **Note:** In practice, the output threshold of a sigmoid prediction probability doesn't necessarily have to 0.5. For example, through testing, you may find that a cut off of 0.25 is better for your chosen evaluation metrics. A common example of this threshold cutoff is the [precision-recall tradeoff](https://en.wikipedia.org/wiki/Precision_and_recall#Introduction) (search for the keyword \"tradeoff\" to learn about the phenomenon)."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 41,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "Qf-R_1vsz47P",
        "outputId": "b42abf4f-219b-46a6-ad23-eeb0255b1084"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "<tf.Tensor: shape=(20,), dtype=float32, numpy=\n",
              "array([0., 1., 1., 0., 0., 1., 1., 1., 1., 0., 0., 1., 0., 0., 0., 0., 0.,\n",
              "       0., 0., 1.], dtype=float32)>"
            ]
          },
          "metadata": {},
          "execution_count": 41
        }
      ],
      "source": [
        "# Turn prediction probabilities into single-dimension tensor of floats\n",
        "model_1_preds = tf.squeeze(tf.round(model_1_pred_probs)) # squeeze removes single dimensions\n",
        "model_1_preds[:20]"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "Zc3ryY0yCHcI"
      },
      "source": [
        "Now we've got our model's predictions in the form of classes, we can use our `calculate_results()` function to compare them to the ground truth validation labels."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 42,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "iDEEhYTF0X1y",
        "outputId": "8c3fa99e-d247-4c31-feb4-60f75968a406"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "{'accuracy': 78.60892388451444,\n",
              " 'precision': 0.7903277546022673,\n",
              " 'recall': 0.7860892388451444,\n",
              " 'f1': 0.7832971347503846}"
            ]
          },
          "metadata": {},
          "execution_count": 42
        }
      ],
      "source": [
        "# Calculate model_1 metrics\n",
        "model_1_results = calculate_results(y_true=val_labels, \n",
        "                                    y_pred=model_1_preds)\n",
        "model_1_results"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "gnkK6Uc7CYlX"
      },
      "source": [
        "How about we compare our first deep model to our baseline model?"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 43,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "Jp88ystW1m0d",
        "outputId": "e06d9d8e-ceec-413f-e3a1-6e026150fb8e"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "array([False, False, False, False])"
            ]
          },
          "metadata": {},
          "execution_count": 43
        }
      ],
      "source": [
        "# Is our simple Keras model better than our baseline model?\n",
        "import numpy as np\n",
        "np.array(list(model_1_results.values())) > np.array(list(baseline_results.values()))"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "lUINrCdRCpFf"
      },
      "source": [
        "Since we'll be doing this kind of comparison (baseline compared to new model) quite a few times, let's create a function to help us out. "
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 44,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "wo3norTG3GrE",
        "outputId": "902c6819-c78e-4dba-b1f8-a8cfad0db80d"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Baseline accuracy: 79.27, New accuracy: 78.61, Difference: -0.66\n",
            "Baseline precision: 0.81, New precision: 0.79, Difference: -0.02\n",
            "Baseline recall: 0.79, New recall: 0.79, Difference: -0.01\n",
            "Baseline f1: 0.79, New f1: 0.78, Difference: -0.00\n"
          ]
        }
      ],
      "source": [
        "# Create a helper function to compare our baseline results to new model results\n",
        "def compare_baseline_to_new_results(baseline_results, new_model_results):\n",
        "  for key, value in baseline_results.items():\n",
        "    print(f\"Baseline {key}: {value:.2f}, New {key}: {new_model_results[key]:.2f}, Difference: {new_model_results[key]-value:.2f}\")\n",
        "\n",
        "compare_baseline_to_new_results(baseline_results=baseline_results, \n",
        "                                new_model_results=model_1_results)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "6e-1LuioSLAM"
      },
      "source": [
        "## Visualizing learned embeddings\n",
        "\n",
        "Our first model (`model_1`) contained an embedding layer (`embedding`) which learned a way of representing words as feature vectors by passing over the training data.\n",
        "\n",
        "Hearing this for the first few times may sound confusing.\n",
        "\n",
        "So to further help understand what a text embedding is, let's visualize the embedding our model learned.\n",
        "\n",
        "To do so, let's remind ourselves of the words in our vocabulary.\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 45,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "-DkcfRQBVXuJ",
        "outputId": "444cef16-b633-4e9b-aa68-c9d3929fd02a"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "(10000, ['', '[UNK]', 'the', 'a', 'in', 'to', 'of', 'and', 'i', 'is'])"
            ]
          },
          "metadata": {},
          "execution_count": 45
        }
      ],
      "source": [
        "# Get the vocabulary from the text vectorization layer\n",
        "words_in_vocab = text_vectorizer.get_vocabulary()\n",
        "len(words_in_vocab), words_in_vocab[:10]"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "KzmAPJXQEx6r"
      },
      "source": [
        "And now let's get our embedding layer's weights (these are the numerical representations of each word)."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 46,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "8EUR9PwrZphh",
        "outputId": "4a0ec496-528b-4f33-eb36-a2f2b69f443a"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Model: \"model_1_dense\"\n",
            "_________________________________________________________________\n",
            " Layer (type)                Output Shape              Param #   \n",
            "=================================================================\n",
            " input_1 (InputLayer)        [(None, 1)]               0         \n",
            "                                                                 \n",
            " text_vectorization_1 (TextV  (None, 15)               0         \n",
            " ectorization)                                                   \n",
            "                                                                 \n",
            " embedding_1 (Embedding)     (None, 15, 128)           1280000   \n",
            "                                                                 \n",
            " global_average_pooling1d (G  (None, 128)              0         \n",
            " lobalAveragePooling1D)                                          \n",
            "                                                                 \n",
            " dense (Dense)               (None, 1)                 129       \n",
            "                                                                 \n",
            "=================================================================\n",
            "Total params: 1,280,129\n",
            "Trainable params: 1,280,129\n",
            "Non-trainable params: 0\n",
            "_________________________________________________________________\n"
          ]
        }
      ],
      "source": [
        "model_1.summary()"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 47,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "9xJ5LrInWDLo",
        "outputId": "f315c987-e355-4aac-ea9a-b2230d4f35fd"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "(10000, 128)\n"
          ]
        }
      ],
      "source": [
        "# Get the weight matrix of embedding layer \n",
        "# (these are the numerical patterns between the text in the training dataset the model has learned)\n",
        "embed_weights = model_1.get_layer(\"embedding_1\").get_weights()[0]\n",
        "print(embed_weights.shape) # same size as vocab size and embedding_dim (each word is a embedding_dim size vector)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "jzOJhJHPW1ju"
      },
      "source": [
        "Now we've got these two objects, we can use the [Embedding Projector tool](http://projector.tensorflow.org/_) to visualize our embedding. \n",
        "\n",
        "To use the Embedding Projector tool, we need two files:\n",
        "* The embedding vectors (same as embedding weights).\n",
        "* The meta data of the embedding vectors (the words they represent - our vocabulary).\n",
        "\n",
        "Right now, we've got of these files as Python objects. To download them to file, we're going to [use the code example available on the TensorFlow word embeddings tutorial page](https://www.tensorflow.org/tutorials/text/word_embeddings#retrieve_the_trained_word_embeddings_and_save_them_to_disk).\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 48,
      "metadata": {
        "id": "4e9rfcK6WxQE"
      },
      "outputs": [],
      "source": [
        "# # Code below is adapted from: https://www.tensorflow.org/tutorials/text/word_embeddings#retrieve_the_trained_word_embeddings_and_save_them_to_disk\n",
        "# import io\n",
        "\n",
        "# # Create output writers\n",
        "# out_v = io.open(\"embedding_vectors.tsv\", \"w\", encoding=\"utf-8\")\n",
        "# out_m = io.open(\"embedding_metadata.tsv\", \"w\", encoding=\"utf-8\")\n",
        "\n",
        "# # Write embedding vectors and words to file\n",
        "# for num, word in enumerate(words_in_vocab):\n",
        "#   if num == 0: \n",
        "#      continue # skip padding token\n",
        "#   vec = embed_weights[num]\n",
        "#   out_m.write(word + \"\\n\") # write words to file\n",
        "#   out_v.write(\"\\t\".join([str(x) for x in vec]) + \"\\n\") # write corresponding word vector to file\n",
        "# out_v.close()\n",
        "# out_m.close()\n",
        "\n",
        "# # Download files locally to upload to Embedding Projector\n",
        "# try:\n",
        "#   from google.colab import files\n",
        "# except ImportError:\n",
        "#   pass\n",
        "# else:\n",
        "#   files.download(\"embedding_vectors.tsv\")\n",
        "#   files.download(\"embedding_metadata.tsv\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "BVM7ifzpZaxJ"
      },
      "source": [
        "Once you've downloaded the embedding vectors and metadata, you can visualize them using Embedding Vector tool:\n",
        "1. Go to  http://projector.tensorflow.org/\n",
        "2. Click on \"Load data\"\n",
        "3. Upload the two files you downloaded (`embedding_vectors.tsv` and `embedding_metadata.tsv`)\n",
        "4. Explore\n",
        "5. Optional: You can share the data you've created by clicking \"Publish\"\n",
        "\n",
        "What do you find?\n",
        "\n",
        "Are words with similar meanings close together?\n",
        "\n",
        "Remember, they might not be. The embeddings we downloaded are how our model interprets words, not necessarily how we interpret them. \n",
        "\n",
        "Also, since the embedding has been learned purely from Tweets, it may contain some strange values as Tweets are a very unique style of natural language.\n",
        "\n",
        "> 🤔 **Question:** Do you have to visualize embeddings every time?\n",
        "\n",
        "No. Although helpful for gaining an intuition of what natural language embeddings are, it's not completely necessary. Especially as the dimensions of your vocabulary and embeddings grow, trying to comprehend them would become an increasingly difficult task."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "AcRdDiEtGQj4"
      },
      "source": [
        "## Recurrent Neural Networks (RNN's)\n",
        "\n",
        "For our next series of modelling experiments we're going to be using a special kind of neural network called a **Recurrent Neural Network (RNN)**.\n",
        "\n",
        "The premise of an RNN is simple: use information from the past to help you with the future (this is where the term recurrent comes from). In other words, take an input (`X`) and compute an output (`y`) based on all previous inputs.\n",
        "\n",
        "This concept is especially helpful when dealing with sequences such as passages of natural language text (such as our Tweets).\n",
        "\n",
        "For example, when you read this sentence, you take into context the previous words when deciphering the meaning of the current word dog. \n",
        "\n",
        "See what happened there? \n",
        "\n",
        "I put the word \"dog\" at the end which is a valid word but it doesn't make sense in the context of the rest of the sentence.\n",
        "\n",
        "When an RNN looks at a sequence of text (already in numerical form), the patterns it learns are continually updated based on the order of the sequence. \n",
        "\n",
        "For a simple example, take two sentences:\n",
        "1. Massive earthquake last week, no?\n",
        "2. No massive earthquake last week.\n",
        "\n",
        "Both contain exactly the same words but have different meaning. The order of the words determines the meaning (one could argue punctuation marks also dictate the meaning but for simplicity sake, let's stay focused on the words).\n",
        "\n",
        "Recurrent neural networks can be used for a number of sequence-based problems:\n",
        "* **One to one:** one input, one output, such as image classification.\n",
        "* **One to many:** one input, many outputs, such as image captioning (image input, a sequence of text as caption output).\n",
        "* **Many to one:** many inputs, one outputs, such as text classification (classifying a Tweet as real diaster or not real diaster).\n",
        "* **Many to many:** many inputs, many outputs, such as machine translation (translating English to Spanish) or speech to text (audio wave as input, text as output).\n",
        "\n",
        "When you come across RNN's in the wild, you'll most likely come across variants of the following:\n",
        "* Long short-term memory cells (LSTMs).\n",
        "* Gated recurrent units (GRUs).\n",
        "* Bidirectional RNN's (passes forward and backward along a sequence, left to right and right to left).\n",
        "\n",
        "Going into the details of each these is beyond the scope of this notebook (we're going to focus on using them instead), the main thing you should know for now is that they've proven very effective at modelling sequences.\n",
        "\n",
        "For a deeper understanding of what's happening behind the scenes of the code we're about to write, I'd recommend the following resources:\n",
        "\n",
        "> 📖 **Resources:**\n",
        "> * [MIT Deep Learning Lecture on Recurrent Neural Networks](https://youtu.be/SEnXr6v2ifU) - explains the background of recurrent neural networks and introduces LSTMs.\n",
        "> * [The Unreasonable Effectiveness of Recurrent Neural Networks](http://karpathy.github.io/2015/05/21/rnn-effectiveness/) by Andrej Karpathy - demonstrates the power of RNN's with examples generating various sequences.\n",
        "> * [Understanding LSTMs](https://colah.github.io/posts/2015-08-Understanding-LSTMs/) by Chris Olah - an in-depth (and technical) look at the mechanics of the LSTM cell, possibly the most popular RNN building block.\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "tDERKwP_XWro"
      },
      "source": [
        "### Model 2: LSTM\n",
        "\n",
        "With all this talk of what RNN's are and what they're good for, I'm sure you're eager to build one.\n",
        "\n",
        "We're going to start with an LSTM-powered RNN.\n",
        "\n",
        "To harness the power of the LSTM cell (LSTM cell and LSTM layer are often used interchangably) in TensorFlow, we'll use [`tensorflow.keras.layers.LSTM()`](https://www.tensorflow.org/api_docs/python/tf/keras/layers/LSTM).\n",
        "\n",
        "![](https://raw.githubusercontent.com/mrdbourke/tensorflow-deep-learning/main/images/08-RNN-architecture-coloured-block-edition.png)\n",
        "*Coloured block example of the structure of an recurrent neural network.*\n",
        "\n",
        "Our model is going to take on a very similar structure to `model_1`:\n",
        "\n",
        "```\n",
        "Input (text) -> Tokenize -> Embedding -> Layers -> Output (label probability)\n",
        "```\n",
        "\n",
        "The main difference will be that we're going to add an LSTM layer between our embedding and output.\n",
        "\n",
        "And to make sure we're not getting reusing trained embeddings (this would involve data leakage between models, leading to an uneven comparison later on), we'll create another embedding layer (`model_2_embedding`) for our model. The `text_vectorizer` layer can be reused since it doesn't get updated during training.\n",
        "\n",
        "> 🔑 **Note:** The reason we use a new embedding layer for each model is since the embedding layer is a *learned* representation of words (as numbers), if we were to use the same embedding layer (`embedding_1`) for each model, we'd be mixing what one model learned with the next. And because we want to compare our models later on, starting them with their own embedding layer each time is a better idea."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 49,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "Pi3vjpFU46hi",
        "outputId": "f677d33f-a235-4a72-b2f9-315cb845f3dd"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "(None, 15, 128)\n",
            "(None, 64)\n"
          ]
        }
      ],
      "source": [
        "# Set random seed and create embedding layer (new embedding layer for each model)\n",
        "tf.random.set_seed(42)\n",
        "from tensorflow.keras import layers\n",
        "model_2_embedding = layers.Embedding(input_dim=max_vocab_length,\n",
        "                                     output_dim=128,\n",
        "                                     embeddings_initializer=\"uniform\",\n",
        "                                     input_length=max_length,\n",
        "                                     name=\"embedding_2\")\n",
        "\n",
        "\n",
        "# Create LSTM model\n",
        "inputs = layers.Input(shape=(1,), dtype=\"string\")\n",
        "x = text_vectorizer(inputs)\n",
        "x = model_2_embedding(x)\n",
        "print(x.shape)\n",
        "# x = layers.LSTM(64, return_sequences=True)(x) # return vector for each word in the Tweet (you can stack RNN cells as long as return_sequences=True)\n",
        "x = layers.LSTM(64)(x) # return vector for whole sequence\n",
        "print(x.shape)\n",
        "# x = layers.Dense(64, activation=\"relu\")(x) # optional dense layer on top of output of LSTM cell\n",
        "outputs = layers.Dense(1, activation=\"sigmoid\")(x)\n",
        "model_2 = tf.keras.Model(inputs, outputs, name=\"model_2_LSTM\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "e1wfTARuwWDg"
      },
      "source": [
        "> 🔑 **Note:** Reading the documentation for the [TensorFlow LSTM layer](https://www.tensorflow.org/api_docs/python/tf/keras/layers/LSTM), you'll find a plethora of parameters. Many of these have been tuned to make sure they compute as fast as possible. The main ones you'll be looking to adjust are `units` (number of hidden units) and `return_sequences` (set this to `True` when stacking LSTM or other recurrent layers).\n",
        "\n",
        "Now we've got our LSTM model built, let's compile it using `\"binary_crossentropy\"` loss and the Adam optimizer."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 50,
      "metadata": {
        "id": "pWdt3bFRwG6w"
      },
      "outputs": [],
      "source": [
        "# Compile model\n",
        "model_2.compile(loss=\"binary_crossentropy\",\n",
        "                optimizer=tf.keras.optimizers.Adam(),\n",
        "                metrics=[\"accuracy\"])"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "I2e_t8RFxgXG"
      },
      "source": [
        "And before we fit our model to the data, let's get a summary."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 51,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "IAjdfDfLwK_R",
        "outputId": "f150e74a-26f2-40bc-d5c1-083bc82e4ef4"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Model: \"model_2_LSTM\"\n",
            "_________________________________________________________________\n",
            " Layer (type)                Output Shape              Param #   \n",
            "=================================================================\n",
            " input_2 (InputLayer)        [(None, 1)]               0         \n",
            "                                                                 \n",
            " text_vectorization_1 (TextV  (None, 15)               0         \n",
            " ectorization)                                                   \n",
            "                                                                 \n",
            " embedding_2 (Embedding)     (None, 15, 128)           1280000   \n",
            "                                                                 \n",
            " lstm (LSTM)                 (None, 64)                49408     \n",
            "                                                                 \n",
            " dense_1 (Dense)             (None, 1)                 65        \n",
            "                                                                 \n",
            "=================================================================\n",
            "Total params: 1,329,473\n",
            "Trainable params: 1,329,473\n",
            "Non-trainable params: 0\n",
            "_________________________________________________________________\n"
          ]
        }
      ],
      "source": [
        "model_2.summary()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "S5NLw3wD0aMz"
      },
      "source": [
        "Looking good! You'll notice a fair few more trainable parameters within our LSTM layer than `model_1`. \n",
        "\n",
        "If you'd like to know where this number comes from, I recommend going through the above resources as well the following on calculating the number of parameters in an LSTM cell:\n",
        "* [Stack Overflow answer to calculate the number of parameters in an LSTM cell](https://stackoverflow.com/questions/38080035/how-to-calculate-the-number-of-parameters-of-an-lstm-network) by Marcin Możejko\n",
        "* [Calculating number of parameters in a LSTM unit and layer](https://medium.com/@priyadarshi.cse/calculating-number-of-parameters-in-a-lstm-unit-layer-7e491978e1e4) by Shridhar Priyadarshi\n",
        "\n",
        "Now our first RNN model's compiled let's fit it to our training data, validating it on the validation data and tracking its training parameters using our TensorBoard callback."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 52,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "YgZ7ojDvwKcq",
        "outputId": "d25ace18-6b55-43ee-f1d5-fb6b3bea620c"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Saving TensorBoard log files to: model_logs/LSTM/20230526-001518\n",
            "Epoch 1/5\n",
            "215/215 [==============================] - 13s 44ms/step - loss: 0.5074 - accuracy: 0.7460 - val_loss: 0.4590 - val_accuracy: 0.7743\n",
            "Epoch 2/5\n",
            "215/215 [==============================] - 2s 10ms/step - loss: 0.3168 - accuracy: 0.8716 - val_loss: 0.5119 - val_accuracy: 0.7756\n",
            "Epoch 3/5\n",
            "215/215 [==============================] - 2s 10ms/step - loss: 0.2198 - accuracy: 0.9155 - val_loss: 0.5876 - val_accuracy: 0.7677\n",
            "Epoch 4/5\n",
            "215/215 [==============================] - 2s 8ms/step - loss: 0.1577 - accuracy: 0.9442 - val_loss: 0.5923 - val_accuracy: 0.7795\n",
            "Epoch 5/5\n",
            "215/215 [==============================] - 2s 8ms/step - loss: 0.1108 - accuracy: 0.9577 - val_loss: 0.8550 - val_accuracy: 0.7559\n"
          ]
        }
      ],
      "source": [
        "# Fit model\n",
        "model_2_history = model_2.fit(train_sentences,\n",
        "                              train_labels,\n",
        "                              epochs=5,\n",
        "                              validation_data=(val_sentences, val_labels),\n",
        "                              callbacks=[create_tensorboard_callback(SAVE_DIR, \n",
        "                                                                     \"LSTM\")])"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "1gikGe_Z16PP"
      },
      "source": [
        "Nice! We've got our first trained RNN model using LSTM cells. Let's make some predictions with it.\n",
        "\n",
        "The same thing will happen as before, due to the sigmoid activiation function in the final layer, when we call the `predict()` method on our model, it'll return prediction probabilities rather than classes."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 53,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "4c_lVbKLemrU",
        "outputId": "02ef8004-44e7-4e92-e221-6bbd1623a975"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "24/24 [==============================] - 0s 2ms/step\n"
          ]
        },
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "((762, 1),\n",
              " array([[0.00630066],\n",
              "        [0.7862389 ],\n",
              "        [0.9991792 ],\n",
              "        [0.06841089],\n",
              "        [0.00448257],\n",
              "        [0.99932086],\n",
              "        [0.8617405 ],\n",
              "        [0.99968505],\n",
              "        [0.9993248 ],\n",
              "        [0.57989997]], dtype=float32))"
            ]
          },
          "metadata": {},
          "execution_count": 53
        }
      ],
      "source": [
        "# Make predictions on the validation dataset\n",
        "model_2_pred_probs = model_2.predict(val_sentences)\n",
        "model_2_pred_probs.shape, model_2_pred_probs[:10] # view the first 10"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "fQ6ope-ddpOo"
      },
      "source": [
        "We can turn these prediction probabilities into prediction classes by rounding to the nearest integer (by default, prediction probabilities under 0.5 will go to 0 and those over 0.5 will go to 1)."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 54,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "iFnIhtyE7hlb",
        "outputId": "b732247d-1378-42a2-ab1a-bf5bc332aeca"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "<tf.Tensor: shape=(10,), dtype=float32, numpy=array([0., 1., 1., 0., 0., 1., 1., 1., 1., 1.], dtype=float32)>"
            ]
          },
          "metadata": {},
          "execution_count": 54
        }
      ],
      "source": [
        "# Round out predictions and reduce to 1-dimensional array\n",
        "model_2_preds = tf.squeeze(tf.round(model_2_pred_probs))\n",
        "model_2_preds[:10]"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "zTBy4poXd_7p"
      },
      "source": [
        "Beautiful, now let's use our `caculate_results()` function to evaluate our LSTM model and our `compare_baseline_to_new_results()` function to compare it to our baseline model."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 55,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "3iHXv04y76vj",
        "outputId": "17b80b37-82bd-40e7-c86b-73dc2eb26e39"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "{'accuracy': 75.59055118110236,\n",
              " 'precision': 0.7567160722556739,\n",
              " 'recall': 0.7559055118110236,\n",
              " 'f1': 0.7539595513230887}"
            ]
          },
          "metadata": {},
          "execution_count": 55
        }
      ],
      "source": [
        "# Calculate LSTM model results\n",
        "model_2_results = calculate_results(y_true=val_labels,\n",
        "                                    y_pred=model_2_preds)\n",
        "model_2_results"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 56,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "ZdQGn2L68B5Q",
        "outputId": "1011e2a5-496f-4a1d-a9d7-d331d047a219"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Baseline accuracy: 79.27, New accuracy: 75.59, Difference: -3.67\n",
            "Baseline precision: 0.81, New precision: 0.76, Difference: -0.05\n",
            "Baseline recall: 0.79, New recall: 0.76, Difference: -0.04\n",
            "Baseline f1: 0.79, New f1: 0.75, Difference: -0.03\n"
          ]
        }
      ],
      "source": [
        "# Compare model 2 to baseline\n",
        "compare_baseline_to_new_results(baseline_results, model_2_results)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "Q0pAtADt8ju7"
      },
      "source": [
        "### Model 3: GRU\n",
        "\n",
        "Another popular and effective RNN component is the GRU or gated recurrent unit.\n",
        "\n",
        "The GRU cell has similar features to an LSTM cell but has less parameters.\n",
        "\n",
        "> 📖 **Resource:** A full explanation of the GRU cell is beyond the scope of this noteook but I'd suggest the following resources to learn more:\n",
        "* [Gated Recurrent Unit](https://en.wikipedia.org/wiki/Gated_recurrent_unit) Wikipedia page\n",
        "* [Understanding GRU networks](https://towardsdatascience.com/understanding-gru-networks-2ef37df6c9be) by Simeon Kostadinov\n",
        "\n",
        "To use the GRU cell in TensorFlow, we can call the [`tensorflow.keras.layers.GRU()`](https://www.tensorflow.org/api_docs/python/tf/keras/layers/GRU) class.\n",
        "\n",
        "The architecture of the GRU-powered model will follow the same structure we've been using:\n",
        "\n",
        "```\n",
        "Input (text) -> Tokenize -> Embedding -> Layers -> Output (label probability)\n",
        "```\n",
        "\n",
        "Again, the only difference will be the layer(s) we use between the embedding and the output."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 57,
      "metadata": {
        "id": "SoSCGq3H47Yo"
      },
      "outputs": [],
      "source": [
        "# Set random seed and create embedding layer (new embedding layer for each model)\n",
        "tf.random.set_seed(42)\n",
        "from tensorflow.keras import layers\n",
        "model_3_embedding = layers.Embedding(input_dim=max_vocab_length,\n",
        "                                     output_dim=128,\n",
        "                                     embeddings_initializer=\"uniform\",\n",
        "                                     input_length=max_length,\n",
        "                                     name=\"embedding_3\")\n",
        "\n",
        "# Build an RNN using the GRU cell\n",
        "inputs = layers.Input(shape=(1,), dtype=\"string\")\n",
        "x = text_vectorizer(inputs)\n",
        "x = model_3_embedding(x)\n",
        "# x = layers.GRU(64, return_sequences=True) # stacking recurrent cells requires return_sequences=True\n",
        "x = layers.GRU(64)(x) \n",
        "# x = layers.Dense(64, activation=\"relu\")(x) # optional dense layer after GRU cell\n",
        "outputs = layers.Dense(1, activation=\"sigmoid\")(x)\n",
        "model_3 = tf.keras.Model(inputs, outputs, name=\"model_3_GRU\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "JLT5maFWhKH1"
      },
      "source": [
        "TensorFlow makes it easy to use powerful components such as the GRU cell in our models. And now our third model is built, let's compile it, just as before."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 58,
      "metadata": {
        "id": "lBL1mb31hHDS"
      },
      "outputs": [],
      "source": [
        "# Compile GRU model\n",
        "model_3.compile(loss=\"binary_crossentropy\",\n",
        "                optimizer=tf.keras.optimizers.Adam(),\n",
        "                metrics=[\"accuracy\"])"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "yvnksvkmha2A"
      },
      "source": [
        "What does a summary of our model look like?"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 59,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "JVnB5yQeiAWs",
        "outputId": "a730270e-0857-4fbb-a74c-3471f8d26639"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Model: \"model_3_GRU\"\n",
            "_________________________________________________________________\n",
            " Layer (type)                Output Shape              Param #   \n",
            "=================================================================\n",
            " input_3 (InputLayer)        [(None, 1)]               0         \n",
            "                                                                 \n",
            " text_vectorization_1 (TextV  (None, 15)               0         \n",
            " ectorization)                                                   \n",
            "                                                                 \n",
            " embedding_3 (Embedding)     (None, 15, 128)           1280000   \n",
            "                                                                 \n",
            " gru (GRU)                   (None, 64)                37248     \n",
            "                                                                 \n",
            " dense_2 (Dense)             (None, 1)                 65        \n",
            "                                                                 \n",
            "=================================================================\n",
            "Total params: 1,317,313\n",
            "Trainable params: 1,317,313\n",
            "Non-trainable params: 0\n",
            "_________________________________________________________________\n"
          ]
        }
      ],
      "source": [
        "# Get a summary of the GRU model\n",
        "model_3.summary()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "KcXzKqgXhdez"
      },
      "source": [
        "Notice the difference in number of trainable parameters between `model_2` (LSTM) and `model_3` (GRU). The difference comes from the LSTM cell having more trainable parameters than the GRU cell.\n",
        "\n",
        "We'll fit our model just as we've been doing previously. We'll also track our models results using our `create_tensorboard_callback()` function."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 60,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "Gvamg5JOh_jC",
        "outputId": "cfb59be7-625b-4f75-abb6-6db6c9b003ed"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Saving TensorBoard log files to: model_logs/GRU/20230526-001539\n",
            "Epoch 1/5\n",
            "215/215 [==============================] - 11s 43ms/step - loss: 0.5274 - accuracy: 0.7231 - val_loss: 0.4539 - val_accuracy: 0.7795\n",
            "Epoch 2/5\n",
            "215/215 [==============================] - 2s 10ms/step - loss: 0.3179 - accuracy: 0.8686 - val_loss: 0.4850 - val_accuracy: 0.7848\n",
            "Epoch 3/5\n",
            "215/215 [==============================] - 2s 9ms/step - loss: 0.2149 - accuracy: 0.9187 - val_loss: 0.5544 - val_accuracy: 0.7717\n",
            "Epoch 4/5\n",
            "215/215 [==============================] - 2s 8ms/step - loss: 0.1517 - accuracy: 0.9488 - val_loss: 0.6279 - val_accuracy: 0.7835\n",
            "Epoch 5/5\n",
            "215/215 [==============================] - 2s 8ms/step - loss: 0.1145 - accuracy: 0.9609 - val_loss: 0.6063 - val_accuracy: 0.7756\n"
          ]
        }
      ],
      "source": [
        "# Fit model\n",
        "model_3_history = model_3.fit(train_sentences,\n",
        "                              train_labels,\n",
        "                              epochs=5,\n",
        "                              validation_data=(val_sentences, val_labels),\n",
        "                              callbacks=[create_tensorboard_callback(SAVE_DIR, \"GRU\")])"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "hM4mQj1Sh7Gn"
      },
      "source": [
        "Due to the optimized default settings of the GRU cell in TensorFlow, training doesn't take long at all. \n",
        "\n",
        "Time to make some predictions on the validation samples."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 61,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "W5TUVHCl9pe-",
        "outputId": "da5a388d-37ac-41fc-a0de-bc5d50f5abea"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "24/24 [==============================] - 0s 2ms/step\n"
          ]
        },
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "((762, 1),\n",
              " array([[0.31703022],\n",
              "        [0.9160779 ],\n",
              "        [0.9977792 ],\n",
              "        [0.14830083],\n",
              "        [0.01086212],\n",
              "        [0.9908326 ],\n",
              "        [0.6938264 ],\n",
              "        [0.9978917 ],\n",
              "        [0.99662066],\n",
              "        [0.4299642 ]], dtype=float32))"
            ]
          },
          "metadata": {},
          "execution_count": 61
        }
      ],
      "source": [
        "# Make predictions on the validation data\n",
        "model_3_pred_probs = model_3.predict(val_sentences)\n",
        "model_3_pred_probs.shape, model_3_pred_probs[:10]"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "hasS7dzRiYQh"
      },
      "source": [
        "Again we get an array of prediction probabilities back which we can convert to prediction classes by rounding them."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 62,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "haILbddg98CY",
        "outputId": "d7a9ee73-a219-4288-9ef4-46d999cc4e25"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "<tf.Tensor: shape=(10,), dtype=float32, numpy=array([0., 1., 1., 0., 0., 1., 1., 1., 1., 0.], dtype=float32)>"
            ]
          },
          "metadata": {},
          "execution_count": 62
        }
      ],
      "source": [
        "# Convert prediction probabilities to prediction classes\n",
        "model_3_preds = tf.squeeze(tf.round(model_3_pred_probs))\n",
        "model_3_preds[:10]"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "_7yAgh-viglB"
      },
      "source": [
        "Now we've got predicted classes, let's evaluate them against the ground truth labels."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 63,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "h9OZbQu1-LPp",
        "outputId": "b64a3f45-10c5-416e-d362-33d994fe35ec"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "{'accuracy': 77.55905511811024,\n",
              " 'precision': 0.776326889347514,\n",
              " 'recall': 0.7755905511811023,\n",
              " 'f1': 0.7740902496040959}"
            ]
          },
          "metadata": {},
          "execution_count": 63
        }
      ],
      "source": [
        "# Calcuate model_3 results\n",
        "model_3_results = calculate_results(y_true=val_labels, \n",
        "                                    y_pred=model_3_preds)\n",
        "model_3_results"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "o9t7wcALiuRk"
      },
      "source": [
        "Finally we can compare our GRU model's results to our baseline."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 64,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "_7AE6vtn-RQZ",
        "outputId": "c39a27c9-e6a9-40a6-f3b7-05d6ffd20066"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Baseline accuracy: 79.27, New accuracy: 77.56, Difference: -1.71\n",
            "Baseline precision: 0.81, New precision: 0.78, Difference: -0.03\n",
            "Baseline recall: 0.79, New recall: 0.78, Difference: -0.02\n",
            "Baseline f1: 0.79, New f1: 0.77, Difference: -0.01\n"
          ]
        }
      ],
      "source": [
        "# Compare to baseline\n",
        "compare_baseline_to_new_results(baseline_results, model_3_results)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "oLm6r4nQ-Wdr"
      },
      "source": [
        "### Model 4: Bidirectonal RNN model \n",
        "\n",
        "Look at us go! We've already built two RNN's with GRU and LSTM cells. Now we're going to look into another kind of RNN, the bidirectional RNN.\n",
        "\n",
        "A standard RNN will process a sequence from left to right, where as a bidirectional RNN will process the sequence from left to right and then again from right to left.\n",
        "\n",
        "Intuitively, this can be thought of as if you were reading a sentence for the first time in the normal fashion (left to right) but for some reason it didn't make sense so you traverse back through the words and go back over them again (right to left).\n",
        "\n",
        "In practice, many sequence models often see and improvement in performance when using bidirectional RNN's.\n",
        "\n",
        "However, this improvement in performance often comes at the cost of longer training times and increased model parameters (since the model goes left to right and right to left, the number of trainable parameters doubles).\n",
        "\n",
        "Okay enough talk, let's build a bidirectional RNN.\n",
        "\n",
        "Once again, TensorFlow helps us out by providing the [`tensorflow.keras.layers.Bidirectional`](https://www.tensorflow.org/api_docs/python/tf/keras/layers/Bidirectional) class. We can use the `Bidirectional` class to wrap our existing RNNs, instantly making them bidirectional."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 65,
      "metadata": {
        "id": "NAU9dvGm47_2"
      },
      "outputs": [],
      "source": [
        "# Set random seed and create embedding layer (new embedding layer for each model)\n",
        "tf.random.set_seed(42)\n",
        "from tensorflow.keras import layers\n",
        "model_4_embedding = layers.Embedding(input_dim=max_vocab_length,\n",
        "                                     output_dim=128,\n",
        "                                     embeddings_initializer=\"uniform\",\n",
        "                                     input_length=max_length,\n",
        "                                     name=\"embedding_4\")\n",
        "\n",
        "# Build a Bidirectional RNN in TensorFlow\n",
        "inputs = layers.Input(shape=(1,), dtype=\"string\")\n",
        "x = text_vectorizer(inputs)\n",
        "x = model_4_embedding(x)\n",
        "# x = layers.Bidirectional(layers.LSTM(64, return_sequences=True))(x) # stacking RNN layers requires return_sequences=True\n",
        "x = layers.Bidirectional(layers.LSTM(64))(x) # bidirectional goes both ways so has double the parameters of a regular LSTM layer\n",
        "outputs = layers.Dense(1, activation=\"sigmoid\")(x)\n",
        "model_4 = tf.keras.Model(inputs, outputs, name=\"model_4_Bidirectional\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "9Hm5cwmNm-g4"
      },
      "source": [
        "> 🔑 **Note:** You can use the `Bidirectional` wrapper on any RNN cell in TensorFlow. For example, `layers.Bidirectional(layers.GRU(64))` creates a bidirectional GRU cell.\n",
        "\n",
        "Our bidirectional model is built, let's compile it."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 66,
      "metadata": {
        "id": "wP1jeF0am9x0"
      },
      "outputs": [],
      "source": [
        "# Compile\n",
        "model_4.compile(loss=\"binary_crossentropy\",\n",
        "                optimizer=tf.keras.optimizers.Adam(),\n",
        "                metrics=[\"accuracy\"])"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "NtpYyjsbnEwN"
      },
      "source": [
        "And of course, we'll check out a summary."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 67,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "-sUd9AQ6nFXI",
        "outputId": "05cce0cd-22dc-4181-83dd-5e384cbd44aa"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Model: \"model_4_Bidirectional\"\n",
            "_________________________________________________________________\n",
            " Layer (type)                Output Shape              Param #   \n",
            "=================================================================\n",
            " input_4 (InputLayer)        [(None, 1)]               0         \n",
            "                                                                 \n",
            " text_vectorization_1 (TextV  (None, 15)               0         \n",
            " ectorization)                                                   \n",
            "                                                                 \n",
            " embedding_4 (Embedding)     (None, 15, 128)           1280000   \n",
            "                                                                 \n",
            " bidirectional (Bidirectiona  (None, 128)              98816     \n",
            " l)                                                              \n",
            "                                                                 \n",
            " dense_3 (Dense)             (None, 1)                 129       \n",
            "                                                                 \n",
            "=================================================================\n",
            "Total params: 1,378,945\n",
            "Trainable params: 1,378,945\n",
            "Non-trainable params: 0\n",
            "_________________________________________________________________\n"
          ]
        }
      ],
      "source": [
        "# Get a summary of our bidirectional model\n",
        "model_4.summary()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "TvItfzeZnIE-"
      },
      "source": [
        "Notice the increased number of trainable parameters in `model_4` (bidirectional LSTM) compared to `model_2` (regular LSTM). This is due to the bidirectionality we added to our RNN.\n",
        "\n",
        "Time to fit our bidirectional model and track its performance."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 68,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "bAKY_QbHXPHB",
        "outputId": "93ac80ba-901d-4cc4-8b87-9ad052405efd"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Saving TensorBoard log files to: model_logs/bidirectional_RNN/20230526-001559\n",
            "Epoch 1/5\n",
            "215/215 [==============================] - 14s 47ms/step - loss: 0.5096 - accuracy: 0.7447 - val_loss: 0.4585 - val_accuracy: 0.7861\n",
            "Epoch 2/5\n",
            "215/215 [==============================] - 2s 12ms/step - loss: 0.3140 - accuracy: 0.8726 - val_loss: 0.5086 - val_accuracy: 0.7743\n",
            "Epoch 3/5\n",
            "215/215 [==============================] - 2s 11ms/step - loss: 0.2139 - accuracy: 0.9183 - val_loss: 0.5716 - val_accuracy: 0.7730\n",
            "Epoch 4/5\n",
            "215/215 [==============================] - 2s 9ms/step - loss: 0.1486 - accuracy: 0.9504 - val_loss: 0.6707 - val_accuracy: 0.7703\n",
            "Epoch 5/5\n",
            "215/215 [==============================] - 2s 10ms/step - loss: 0.1058 - accuracy: 0.9648 - val_loss: 0.6658 - val_accuracy: 0.7677\n"
          ]
        }
      ],
      "source": [
        "# Fit the model (takes longer because of the bidirectional layers)\n",
        "model_4_history = model_4.fit(train_sentences,\n",
        "                              train_labels,\n",
        "                              epochs=5,\n",
        "                              validation_data=(val_sentences, val_labels),\n",
        "                              callbacks=[create_tensorboard_callback(SAVE_DIR, \"bidirectional_RNN\")])"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "zkt8GVRHoJz6"
      },
      "source": [
        "Due to the bidirectionality of our model we see a slight increase in training time.\n",
        "\n",
        "Not to worry, it's not too dramatic of an increase.\n",
        "\n",
        "Let's make some predictions with it."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 69,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "uFc7QHRtXmn7",
        "outputId": "784c4a65-1786-47b9-eb7b-53b920e6d693"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "24/24 [==============================] - 1s 3ms/step\n"
          ]
        },
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "array([[0.05258294],\n",
              "       [0.8495521 ],\n",
              "       [0.99898857],\n",
              "       [0.15441437],\n",
              "       [0.00566462],\n",
              "       [0.99576193],\n",
              "       [0.952807  ],\n",
              "       [0.9993511 ],\n",
              "       [0.99936384],\n",
              "       [0.19425693]], dtype=float32)"
            ]
          },
          "metadata": {},
          "execution_count": 69
        }
      ],
      "source": [
        "# Make predictions with bidirectional RNN on the validation data\n",
        "model_4_pred_probs = model_4.predict(val_sentences)\n",
        "model_4_pred_probs[:10]"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "L_9HmNIYobDB"
      },
      "source": [
        "And we'll convert them to prediction classes and evaluate them against the ground truth labels and baseline model."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 70,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "G5z8bMdaXw51",
        "outputId": "283d42c0-5e36-4db4-83f2-90fe0c03ff00"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "<tf.Tensor: shape=(10,), dtype=float32, numpy=array([0., 1., 1., 0., 0., 1., 1., 1., 1., 0.], dtype=float32)>"
            ]
          },
          "metadata": {},
          "execution_count": 70
        }
      ],
      "source": [
        "# Convert prediction probabilities to labels\n",
        "model_4_preds = tf.squeeze(tf.round(model_4_pred_probs))\n",
        "model_4_preds[:10]"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 71,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "-a7Ym_vKYAO4",
        "outputId": "12741d8d-6f89-4481-d4e3-7c5bcd009e09"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "{'accuracy': 76.77165354330708,\n",
              " 'precision': 0.7675450859410361,\n",
              " 'recall': 0.7677165354330708,\n",
              " 'f1': 0.7667932666650168}"
            ]
          },
          "metadata": {},
          "execution_count": 71
        }
      ],
      "source": [
        "# Calculate bidirectional RNN model results\n",
        "model_4_results = calculate_results(val_labels, model_4_preds)\n",
        "model_4_results"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 72,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "hAET-LKpYT18",
        "outputId": "7cc1d033-17de-40a8-cde4-4d347c718ac8"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Baseline accuracy: 79.27, New accuracy: 76.77, Difference: -2.49\n",
            "Baseline precision: 0.81, New precision: 0.77, Difference: -0.04\n",
            "Baseline recall: 0.79, New recall: 0.77, Difference: -0.02\n",
            "Baseline f1: 0.79, New f1: 0.77, Difference: -0.02\n"
          ]
        }
      ],
      "source": [
        "# Check to see how the bidirectional model performs against the baseline\n",
        "compare_baseline_to_new_results(baseline_results, model_4_results)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "wcvt_7emuKlR"
      },
      "source": [
        "## Convolutional Neural Networks for Text\n",
        "\n",
        "You might've used convolutional neural networks (CNNs) for images before but they can also be used for sequences.\n",
        "\n",
        "The main difference between using CNNs for images and sequences is the shape of the data. Images come in 2-dimensions (height x width) where as sequences are often 1-dimensional (a string of text).\n",
        "\n",
        "So to use CNNs with sequences, we use a 1-dimensional convolution instead of a 2-dimensional convolution.\n",
        "\n",
        "A typical CNN architecture for sequences will look like the following: \n",
        "\n",
        "```\n",
        "Inputs (text) -> Tokenization -> Embedding -> Layers -> Outputs (class probabilities)\n",
        "```\n",
        "\n",
        "You might be thinking \"that just looks like the architecture layout we've been using for the other models...\"\n",
        "\n",
        "And you'd be right.\n",
        "\n",
        "The difference again is in the layers component. Instead of using an LSTM or GRU cell, we're going to use a [`tensorflow.keras.layers.Conv1D()`](https://www.tensorflow.org/api_docs/python/tf/keras/layers/Conv1D) layer followed by a [`tensorflow.keras.layers.GlobablMaxPool1D()`](https://www.tensorflow.org/api_docs/python/tf/keras/layers/GlobalMaxPool1D) layer.\n",
        "\n",
        "> 📖 **Resource:** The intuition here is explained succinctly in the paper [*Understanding Convolutional Neural Networks for Text Classification*](https://www.aclweb.org/anthology/W18-5408.pdf), where they state that CNNs classify text through the following steps:\n",
        "1. 1-dimensional convolving filters are used as ngram detectors, each filter specializing in a closely-related family of ngrams (an ngram is a collection of n-words, for example, an ngram of 5 might result in \"hello, my name is Daniel\").\n",
        "2. Max-pooling over time extracts the relevant ngrams for making a decision.\n",
        "3. The rest of the network classifies the text based on this information.\n",
        "\n",
        "> \n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "lgXEorf9GWY1"
      },
      "source": [
        "### Model 5: Conv1D\n",
        "\n",
        "Before we build a full 1-dimensional CNN model, let's see a 1-dimensional convolutional layer (also called a **temporal convolution**) in action.\n",
        "\n",
        "We'll first create an embedding of a sample of text and experiment passing it through a `Conv1D()` layer and `GlobalMaxPool1D()` layer."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 73,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "563hl7nPWP_3",
        "outputId": "b62f7679-d040-48f3-b5b8-414d2d7a3555"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "(TensorShape([1, 15, 128]), TensorShape([1, 11, 32]), TensorShape([1, 32]))"
            ]
          },
          "metadata": {},
          "execution_count": 73
        }
      ],
      "source": [
        "# Test out the embedding, 1D convolutional and max pooling\n",
        "embedding_test = embedding(text_vectorizer([\"this is a test sentence\"])) # turn target sentence into embedding\n",
        "conv_1d = layers.Conv1D(filters=32, kernel_size=5, activation=\"relu\") # convolve over target sequence 5 words at a time\n",
        "conv_1d_output = conv_1d(embedding_test) # pass embedding through 1D convolutional layer\n",
        "max_pool = layers.GlobalMaxPool1D() \n",
        "max_pool_output = max_pool(conv_1d_output) # get the most important features\n",
        "embedding_test.shape, conv_1d_output.shape, max_pool_output.shape"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "-WzTeShEemJ2"
      },
      "source": [
        "Notice the output shapes of each layer.\n",
        "\n",
        "The embedding has an output shape dimension of the parameters we set it to (`input_length=15` and `output_dim=128`).\n",
        "\n",
        "The 1-dimensional convolutional layer has an output which has been compressed inline with its parameters. And the same goes for the max pooling layer output.\n",
        "\n",
        "Our text starts out as a string but gets converted to a feature vector of length 64 through various transformation steps (from tokenization to embedding to 1-dimensional convolution to max pool).\n",
        "\n",
        "Let's take a peak at what each of these transformations looks like."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 74,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "gRcxYgs-dxM8",
        "outputId": "26c972c3-8469-47b9-9c7e-8e8fa85ccd4b"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "(<tf.Tensor: shape=(1, 15, 128), dtype=float32, numpy=\n",
              " array([[[ 0.01675646, -0.03352517,  0.04817378, ..., -0.02946043,\n",
              "          -0.03770737,  0.01220698],\n",
              "         [-0.00607298,  0.06020833, -0.05641982, ...,  0.08325578,\n",
              "          -0.01878556, -0.08398241],\n",
              "         [-0.0362346 ,  0.00904451, -0.03833614, ...,  0.0051756 ,\n",
              "          -0.00220015, -0.0017492 ],\n",
              "         ...,\n",
              "         [-0.01078545,  0.05590528,  0.03125916, ..., -0.0312557 ,\n",
              "          -0.05340781, -0.03800201],\n",
              "         [-0.01078545,  0.05590528,  0.03125916, ..., -0.0312557 ,\n",
              "          -0.05340781, -0.03800201],\n",
              "         [-0.01078545,  0.05590528,  0.03125916, ..., -0.0312557 ,\n",
              "          -0.05340781, -0.03800201]]], dtype=float32)>,\n",
              " <tf.Tensor: shape=(1, 11, 32), dtype=float32, numpy=\n",
              " array([[[0.        , 0.10975833, 0.        , 0.        , 0.        ,\n",
              "          0.06834612, 0.        , 0.02298634, 0.        , 0.        ,\n",
              "          0.        , 0.        , 0.        , 0.        , 0.06889185,\n",
              "          0.08162662, 0.        , 0.        , 0.03804683, 0.        ,\n",
              "          0.        , 0.        , 0.        , 0.00810859, 0.02383356,\n",
              "          0.        , 0.00385817, 0.        , 0.01310921, 0.        ,\n",
              "          0.        , 0.16110645],\n",
              "         [0.05000008, 0.        , 0.03852113, 0.0149918 , 0.03014192,\n",
              "          0.04613257, 0.        , 0.        , 0.        , 0.05233994,\n",
              "          0.        , 0.        , 0.07095916, 0.03590994, 0.        ,\n",
              "          0.        , 0.        , 0.        , 0.        , 0.05599808,\n",
              "          0.04344876, 0.04021783, 0.        , 0.06110618, 0.        ,\n",
              "          0.        , 0.        , 0.00198402, 0.        , 0.03175152,\n",
              "          0.        , 0.04452901],\n",
              "         [0.        , 0.05068349, 0.06747732, 0.        , 0.        ,\n",
              "          0.04893802, 0.        , 0.        , 0.        , 0.        ,\n",
              "          0.        , 0.        , 0.0853087 , 0.01114925, 0.00223987,\n",
              "          0.        , 0.        , 0.        , 0.        , 0.        ,\n",
              "          0.        , 0.        , 0.        , 0.10797694, 0.02317763,\n",
              "          0.        , 0.01130794, 0.        , 0.01777459, 0.        ,\n",
              "          0.        , 0.02142338],\n",
              "         [0.        , 0.01030538, 0.        , 0.        , 0.02127263,\n",
              "          0.06377578, 0.        , 0.        , 0.        , 0.        ,\n",
              "          0.03660904, 0.        , 0.13293687, 0.06086106, 0.        ,\n",
              "          0.        , 0.        , 0.03161986, 0.00114628, 0.02163697,\n",
              "          0.        , 0.        , 0.        , 0.04408561, 0.        ,\n",
              "          0.01193662, 0.        , 0.        , 0.01174912, 0.03890226,\n",
              "          0.        , 0.06139129],\n",
              "         [0.        , 0.        , 0.00959204, 0.        , 0.03472092,\n",
              "          0.03202822, 0.        , 0.        , 0.        , 0.        ,\n",
              "          0.00390257, 0.        , 0.07451484, 0.00349154, 0.        ,\n",
              "          0.        , 0.02155435, 0.        , 0.        , 0.        ,\n",
              "          0.        , 0.        , 0.        , 0.09407972, 0.        ,\n",
              "          0.        , 0.00077316, 0.        , 0.        , 0.        ,\n",
              "          0.        , 0.06074456],\n",
              "         [0.        , 0.03225943, 0.01736662, 0.        , 0.01197381,\n",
              "          0.02301392, 0.        , 0.        , 0.        , 0.00205472,\n",
              "          0.02762672, 0.        , 0.06565619, 0.00253076, 0.        ,\n",
              "          0.        , 0.00745697, 0.        , 0.        , 0.        ,\n",
              "          0.        , 0.        , 0.        , 0.09595221, 0.        ,\n",
              "          0.        , 0.        , 0.        , 0.01910873, 0.        ,\n",
              "          0.        , 0.06507206],\n",
              "         [0.        , 0.03225943, 0.01736662, 0.        , 0.01197381,\n",
              "          0.02301392, 0.        , 0.        , 0.        , 0.00205472,\n",
              "          0.02762672, 0.        , 0.06565619, 0.00253076, 0.        ,\n",
              "          0.        , 0.00745697, 0.        , 0.        , 0.        ,\n",
              "          0.        , 0.        , 0.        , 0.09595221, 0.        ,\n",
              "          0.        , 0.        , 0.        , 0.01910873, 0.        ,\n",
              "          0.        , 0.06507206],\n",
              "         [0.        , 0.03225943, 0.01736662, 0.        , 0.01197381,\n",
              "          0.02301392, 0.        , 0.        , 0.        , 0.00205472,\n",
              "          0.02762672, 0.        , 0.06565619, 0.00253076, 0.        ,\n",
              "          0.        , 0.00745697, 0.        , 0.        , 0.        ,\n",
              "          0.        , 0.        , 0.        , 0.09595221, 0.        ,\n",
              "          0.        , 0.        , 0.        , 0.01910873, 0.        ,\n",
              "          0.        , 0.06507206],\n",
              "         [0.        , 0.03225943, 0.01736662, 0.        , 0.01197381,\n",
              "          0.02301392, 0.        , 0.        , 0.        , 0.00205472,\n",
              "          0.02762672, 0.        , 0.06565619, 0.00253076, 0.        ,\n",
              "          0.        , 0.00745697, 0.        , 0.        , 0.        ,\n",
              "          0.        , 0.        , 0.        , 0.09595221, 0.        ,\n",
              "          0.        , 0.        , 0.        , 0.01910873, 0.        ,\n",
              "          0.        , 0.06507206],\n",
              "         [0.        , 0.03225943, 0.01736662, 0.        , 0.01197381,\n",
              "          0.02301392, 0.        , 0.        , 0.        , 0.00205472,\n",
              "          0.02762672, 0.        , 0.06565619, 0.00253076, 0.        ,\n",
              "          0.        , 0.00745697, 0.        , 0.        , 0.        ,\n",
              "          0.        , 0.        , 0.        , 0.09595221, 0.        ,\n",
              "          0.        , 0.        , 0.        , 0.01910873, 0.        ,\n",
              "          0.        , 0.06507206],\n",
              "         [0.        , 0.03225943, 0.01736662, 0.        , 0.01197381,\n",
              "          0.02301392, 0.        , 0.        , 0.        , 0.00205472,\n",
              "          0.02762672, 0.        , 0.06565619, 0.00253076, 0.        ,\n",
              "          0.        , 0.00745697, 0.        , 0.        , 0.        ,\n",
              "          0.        , 0.        , 0.        , 0.09595221, 0.        ,\n",
              "          0.        , 0.        , 0.        , 0.01910873, 0.        ,\n",
              "          0.        , 0.06507206]]], dtype=float32)>,\n",
              " <tf.Tensor: shape=(1, 32), dtype=float32, numpy=\n",
              " array([[0.05000008, 0.10975833, 0.06747732, 0.0149918 , 0.03472092,\n",
              "         0.06834612, 0.        , 0.02298634, 0.        , 0.05233994,\n",
              "         0.03660904, 0.        , 0.13293687, 0.06086106, 0.06889185,\n",
              "         0.08162662, 0.02155435, 0.03161986, 0.03804683, 0.05599808,\n",
              "         0.04344876, 0.04021783, 0.        , 0.10797694, 0.02383356,\n",
              "         0.01193662, 0.01130794, 0.00198402, 0.01910873, 0.03890226,\n",
              "         0.        , 0.16110645]], dtype=float32)>)"
            ]
          },
          "metadata": {},
          "execution_count": 74
        }
      ],
      "source": [
        "# See the outputs of each layer\n",
        "embedding_test[:1], conv_1d_output[:1], max_pool_output[:1]"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "kMcrthJwg3B2"
      },
      "source": [
        "Alright, we've seen the outputs of several components of a CNN for sequences, let's put them together and construct a full model, compile it (just as we've done with our other models) and get a summary. "
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 75,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "G9aphPWCYkWN",
        "outputId": "ad2da99b-5180-4a1f-edff-040d5570c8fb"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Model: \"model_5_Conv1D\"\n",
            "_________________________________________________________________\n",
            " Layer (type)                Output Shape              Param #   \n",
            "=================================================================\n",
            " input_5 (InputLayer)        [(None, 1)]               0         \n",
            "                                                                 \n",
            " text_vectorization_1 (TextV  (None, 15)               0         \n",
            " ectorization)                                                   \n",
            "                                                                 \n",
            " embedding_5 (Embedding)     (None, 15, 128)           1280000   \n",
            "                                                                 \n",
            " conv1d_1 (Conv1D)           (None, 11, 32)            20512     \n",
            "                                                                 \n",
            " global_max_pooling1d_1 (Glo  (None, 32)               0         \n",
            " balMaxPooling1D)                                                \n",
            "                                                                 \n",
            " dense_4 (Dense)             (None, 1)                 33        \n",
            "                                                                 \n",
            "=================================================================\n",
            "Total params: 1,300,545\n",
            "Trainable params: 1,300,545\n",
            "Non-trainable params: 0\n",
            "_________________________________________________________________\n"
          ]
        }
      ],
      "source": [
        "# Set random seed and create embedding layer (new embedding layer for each model)\n",
        "tf.random.set_seed(42)\n",
        "from tensorflow.keras import layers\n",
        "model_5_embedding = layers.Embedding(input_dim=max_vocab_length,\n",
        "                                     output_dim=128,\n",
        "                                     embeddings_initializer=\"uniform\",\n",
        "                                     input_length=max_length,\n",
        "                                     name=\"embedding_5\")\n",
        "\n",
        "# Create 1-dimensional convolutional layer to model sequences\n",
        "from tensorflow.keras import layers\n",
        "inputs = layers.Input(shape=(1,), dtype=\"string\")\n",
        "x = text_vectorizer(inputs)\n",
        "x = model_5_embedding(x)\n",
        "x = layers.Conv1D(filters=32, kernel_size=5, activation=\"relu\")(x)\n",
        "x = layers.GlobalMaxPool1D()(x)\n",
        "# x = layers.Dense(64, activation=\"relu\")(x) # optional dense layer\n",
        "outputs = layers.Dense(1, activation=\"sigmoid\")(x)\n",
        "model_5 = tf.keras.Model(inputs, outputs, name=\"model_5_Conv1D\")\n",
        "\n",
        "# Compile Conv1D model\n",
        "model_5.compile(loss=\"binary_crossentropy\",\n",
        "                optimizer=tf.keras.optimizers.Adam(),\n",
        "                metrics=[\"accuracy\"])\n",
        "\n",
        "# Get a summary of our 1D convolution model\n",
        "model_5.summary()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "o1Y4BpMGh0jG"
      },
      "source": [
        "Woohoo! Looking great! Notice how the number of trainable parameters for the 1-dimensional convolutional layer is similar to that of the LSTM layer in `model_2`.\n",
        "\n",
        "Let's fit our 1D CNN model to our text data. In line with previous experiments, we'll save its results using our `create_tensorboard_callback()` function."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 76,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "9fzlaKm1ZrMX",
        "outputId": "17d9f01d-529a-4281-c548-06d0597d84b4"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Saving TensorBoard log files to: model_logs/Conv1D/20230526-001626\n",
            "Epoch 1/5\n",
            "215/215 [==============================] - 11s 42ms/step - loss: 0.5693 - accuracy: 0.7108 - val_loss: 0.4736 - val_accuracy: 0.7769\n",
            "Epoch 2/5\n",
            "215/215 [==============================] - 2s 9ms/step - loss: 0.3426 - accuracy: 0.8600 - val_loss: 0.4677 - val_accuracy: 0.7874\n",
            "Epoch 3/5\n",
            "215/215 [==============================] - 2s 9ms/step - loss: 0.2130 - accuracy: 0.9202 - val_loss: 0.5374 - val_accuracy: 0.7677\n",
            "Epoch 4/5\n",
            "215/215 [==============================] - 1s 7ms/step - loss: 0.1366 - accuracy: 0.9564 - val_loss: 0.6076 - val_accuracy: 0.7756\n",
            "Epoch 5/5\n",
            "215/215 [==============================] - 2s 7ms/step - loss: 0.0958 - accuracy: 0.9667 - val_loss: 0.6706 - val_accuracy: 0.7874\n"
          ]
        }
      ],
      "source": [
        "# Fit the model\n",
        "model_5_history = model_5.fit(train_sentences,\n",
        "                              train_labels,\n",
        "                              epochs=5,\n",
        "                              validation_data=(val_sentences, val_labels),\n",
        "                              callbacks=[create_tensorboard_callback(SAVE_DIR, \n",
        "                                                                     \"Conv1D\")])"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "d2up-1tLiXKD"
      },
      "source": [
        "Nice! Thanks to GPU acceleration, our 1D convolutional model trains nice and fast. Let's make some predictions with it and evaluate them just as before."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 77,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "ZHYw5GkxZ2OK",
        "outputId": "26ab77ba-08af-4941-bbe2-ca1ac1a69f8b"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "24/24 [==============================] - 0s 2ms/step\n"
          ]
        },
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "array([[0.7295443 ],\n",
              "       [0.63939744],\n",
              "       [0.9997949 ],\n",
              "       [0.05865377],\n",
              "       [0.0070557 ],\n",
              "       [0.99556965],\n",
              "       [0.90180606],\n",
              "       [0.9973731 ],\n",
              "       [0.99953437],\n",
              "       [0.6327795 ]], dtype=float32)"
            ]
          },
          "metadata": {},
          "execution_count": 77
        }
      ],
      "source": [
        "# Make predictions with model_5\n",
        "model_5_pred_probs = model_5.predict(val_sentences)\n",
        "model_5_pred_probs[:10]"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 78,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "v9YqTtjiaauS",
        "outputId": "8b990978-011f-4dad-fbf7-adef2b1d1573"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "<tf.Tensor: shape=(10,), dtype=float32, numpy=array([1., 1., 1., 0., 0., 1., 1., 1., 1., 1.], dtype=float32)>"
            ]
          },
          "metadata": {},
          "execution_count": 78
        }
      ],
      "source": [
        "# Convert model_5 prediction probabilities to labels\n",
        "model_5_preds = tf.squeeze(tf.round(model_5_pred_probs))\n",
        "model_5_preds[:10]"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 79,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "wMY3s1Pnaj34",
        "outputId": "9e77d9ac-8a54-476a-9b74-5f648ce57b2c"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "{'accuracy': 78.74015748031496,\n",
              " 'precision': 0.7900609457201325,\n",
              " 'recall': 0.7874015748031497,\n",
              " 'f1': 0.7852275674790494}"
            ]
          },
          "metadata": {},
          "execution_count": 79
        }
      ],
      "source": [
        "# Calculate model_5 evaluation metrics \n",
        "model_5_results = calculate_results(y_true=val_labels, \n",
        "                                    y_pred=model_5_preds)\n",
        "model_5_results"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 80,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "wRfF4B6_at8k",
        "outputId": "ad244c98-83c1-404b-f4e4-ac625da44ec8"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Baseline accuracy: 79.27, New accuracy: 78.74, Difference: -0.52\n",
            "Baseline precision: 0.81, New precision: 0.79, Difference: -0.02\n",
            "Baseline recall: 0.79, New recall: 0.79, Difference: -0.01\n",
            "Baseline f1: 0.79, New f1: 0.79, Difference: -0.00\n"
          ]
        }
      ],
      "source": [
        "# Compare model_5 results to baseline \n",
        "compare_baseline_to_new_results(baseline_results, model_5_results)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "g_roVSSRt-7h"
      },
      "source": [
        "## Using Pretrained Embeddings (transfer learning for NLP)\n",
        "\n",
        "For all of the previous deep learning models we've built and trained, we've created and used our own embeddings from scratch each time.\n",
        "\n",
        "However, a common practice is to leverage pretrained embeddings through **transfer learning**. This is one of the main benefits of using deep models: being able to take what one (often larger) model has learned (often on a large amount of data) and adjust it for our own use case.\n",
        "\n",
        "For our next model, instead of using our own embedding layer, we're going to replace it with a pretrained embedding layer.\n",
        "\n",
        "More specifically, we're going to be using the [Universal Sentence Encoder](https://www.aclweb.org/anthology/D18-2029.pdf) from [TensorFlow Hub](https://tfhub.dev/google/universal-sentence-encoder/4) (a great resource containing a plethora of pretrained model resources for a variety of tasks).\n",
        "\n",
        "> 🔑 **Note:** There are many different pretrained text embedding options on TensorFlow Hub, however, some require different levels of text preprocessing than others. Best to experiment with a few and see which best suits your use case.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "R-NQ2MA5GZBo"
      },
      "source": [
        "### Model 6: TensorFlow Hub Pretrained Sentence Encoder\n",
        "\n",
        "The main difference between the embedding layer we created and the Universal Sentence Encoder is that rather than create a word-level embedding, the Universal Sentence Encoder, as you might've guessed, creates a whole sentence-level embedding.\n",
        "\n",
        "Our embedding layer also outputs an a 128 dimensional vector for each word, where as, the Universal Sentence Encoder outputs a 512 dimensional vector for each sentence.\n",
        "\n",
        "![](https://raw.githubusercontent.com/mrdbourke/tensorflow-deep-learning/main/images/08-USE-tensorflow-hub-encoder-decoder-model.png)\n",
        "*The feature extractor model we're building through the eyes of an **encoder/decoder** model.*\n",
        "\n",
        "> 🔑 **Note:** An **encoder** is the name for a model which converts raw data such as text into a numerical representation (feature vector), a **decoder** converts the numerical representation to a desired output.\n",
        "\n",
        "As usual, this is best demonstrated with an example.\n",
        "\n",
        "We can load in a TensorFlow Hub module using the [`hub.load()`](https://www.tensorflow.org/hub/api_docs/python/hub/load) method and passing it the target URL of the module we'd like to use, in our case, it's \"https://tfhub.dev/google/universal-sentence-encoder/4\".\n",
        "\n",
        "Let's load the Universal Sentence Encoder model and test it on a couple of sentences."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 81,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "7piW5jtxbUkV",
        "outputId": "b3e97667-6bdf-4f8c-ff4a-fb425df47a2a"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "tf.Tensor(\n",
            "[-0.01154496  0.02487099  0.0287963  -0.01272263  0.03969951  0.08829075\n",
            "  0.02682647  0.05582222 -0.01078761 -0.00596655  0.00640638 -0.01816132\n",
            "  0.0002885   0.09106605  0.05874373 -0.03175148  0.01510153 -0.05164852\n",
            "  0.0099434  -0.06867751 -0.04210396  0.0267539   0.03008907  0.00320448\n",
            " -0.00336865 -0.04790529  0.02267517 -0.00984557 -0.04066692 -0.01285528\n",
            " -0.04665243  0.05630673 -0.03952145  0.00521895  0.02495948 -0.07011835\n",
            "  0.02873133  0.04945794 -0.00634555 -0.08959357  0.02807156 -0.00809173\n",
            " -0.01363956  0.05998395 -0.1036155  -0.05192674  0.00232459 -0.02326531\n",
            " -0.03752431  0.0333298 ], shape=(50,), dtype=float32)\n"
          ]
        }
      ],
      "source": [
        "# Example of pretrained embedding with universal sentence encoder - https://tfhub.dev/google/universal-sentence-encoder/4\n",
        "import tensorflow_hub as hub\n",
        "embed = hub.load(\"https://tfhub.dev/google/universal-sentence-encoder/4\") # load Universal Sentence Encoder\n",
        "embed_samples = embed([sample_sentence,\n",
        "                      \"When you call the universal sentence encoder on a sentence, it turns it into numbers.\"])\n",
        "\n",
        "print(embed_samples[0][:50])"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 82,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "vvArnKkGb4vu",
        "outputId": "7bf45cb9-58c3-4cf7-8c72-ebbb7043f2f3"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "TensorShape([512])"
            ]
          },
          "metadata": {},
          "execution_count": 82
        }
      ],
      "source": [
        "# Each sentence has been encoded into a 512 dimension vector\n",
        "embed_samples[0].shape"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "ZxYFDkGD-XjF"
      },
      "source": [
        "Passing our sentences to the Universal Sentence Encoder (USE) encodes them from strings to 512 dimensional vectors, which make no sense to us but hopefully make sense to our machine learning models.\n",
        "\n",
        "Speaking of models, let's build one with the USE as our embedding layer.\n",
        "\n",
        "We can convert the TensorFlow Hub USE module into a Keras layer using the [`hub.KerasLayer`](https://www.tensorflow.org/hub/api_docs/python/hub/KerasLayer) class.\n",
        "\n",
        "> 🔑 **Note:** Due to the size of the USE TensorFlow Hub module, it may take a little while to download. Once it's downloaded though, it'll be cached and ready to use. And as with many TensorFlow Hub modules, there is a [\"lite\" version of the USE](https://tfhub.dev/google/universal-sentence-encoder-lite/2) which takes up less space but sacrifices some performance and requires more preprocessing steps. However, depending on your available compute power, the lite version may be better for your application use case."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 83,
      "metadata": {
        "id": "ZcbBj0aXqrs9"
      },
      "outputs": [],
      "source": [
        "# We can use this encoding layer in place of our text_vectorizer and embedding layer\n",
        "sentence_encoder_layer = hub.KerasLayer(\"https://tfhub.dev/google/universal-sentence-encoder/4\",\n",
        "                                        input_shape=[], # shape of inputs coming to our model \n",
        "                                        dtype=tf.string, # data type of inputs coming to the USE layer\n",
        "                                        trainable=False, # keep the pretrained weights (we'll create a feature extractor)\n",
        "                                        name=\"USE\") "
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "WvjQl4p7BO_A"
      },
      "source": [
        "Beautiful! Now we've got the USE as a Keras layer, we can use it in a Keras Sequential model."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 84,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "M_pjIvPuYltA",
        "outputId": "31921c4a-e1c9-4e92-8b41-39b8059f05fb"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Model: \"model_6_USE\"\n",
            "_________________________________________________________________\n",
            " Layer (type)                Output Shape              Param #   \n",
            "=================================================================\n",
            " USE (KerasLayer)            (None, 512)               256797824 \n",
            "                                                                 \n",
            " dense_5 (Dense)             (None, 64)                32832     \n",
            "                                                                 \n",
            " dense_6 (Dense)             (None, 1)                 65        \n",
            "                                                                 \n",
            "=================================================================\n",
            "Total params: 256,830,721\n",
            "Trainable params: 32,897\n",
            "Non-trainable params: 256,797,824\n",
            "_________________________________________________________________\n"
          ]
        }
      ],
      "source": [
        "# Create model using the Sequential API\n",
        "model_6 = tf.keras.Sequential([\n",
        "  sentence_encoder_layer, # take in sentences and then encode them into an embedding\n",
        "  layers.Dense(64, activation=\"relu\"),\n",
        "  layers.Dense(1, activation=\"sigmoid\")\n",
        "], name=\"model_6_USE\")\n",
        "\n",
        "# Compile model\n",
        "model_6.compile(loss=\"binary_crossentropy\",\n",
        "                optimizer=tf.keras.optimizers.Adam(),\n",
        "                metrics=[\"accuracy\"])\n",
        "\n",
        "model_6.summary()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "yukgxOgCCR2Z"
      },
      "source": [
        "Notice the number of paramters in the USE layer, these are the pretrained weights its learned on various text sources (Wikipedia, web news, web question-answer forums, etc, see the [Universal Sentence Encoder paper](https://www.aclweb.org/anthology/D18-2029.pdf) for more).\n",
        "\n",
        "The trainable parameters are only in our output layers, in other words, we're keeping the USE weights frozen and using it as a feature-extractor. We could fine-tune these weights by setting `trainable=True` when creating the `hub.KerasLayer` instance.\n",
        "\n",
        "Now we've got a feature extractor model ready, let's train it and track its results to TensorBoard using our `create_tensorboard_callback()` function."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 85,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "uX9S0YvafybG",
        "outputId": "4cd696e6-6f3a-4ce7-ca18-f9d5524e682d"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Saving TensorBoard log files to: model_logs/tf_hub_sentence_encoder/20230526-001739\n",
            "Epoch 1/5\n",
            "215/215 [==============================] - 6s 11ms/step - loss: 0.5014 - accuracy: 0.7870 - val_loss: 0.4469 - val_accuracy: 0.7992\n",
            "Epoch 2/5\n",
            "215/215 [==============================] - 2s 9ms/step - loss: 0.4145 - accuracy: 0.8140 - val_loss: 0.4359 - val_accuracy: 0.8097\n",
            "Epoch 3/5\n",
            "215/215 [==============================] - 2s 9ms/step - loss: 0.4000 - accuracy: 0.8216 - val_loss: 0.4319 - val_accuracy: 0.8163\n",
            "Epoch 4/5\n",
            "215/215 [==============================] - 2s 9ms/step - loss: 0.3927 - accuracy: 0.8262 - val_loss: 0.4280 - val_accuracy: 0.8176\n",
            "Epoch 5/5\n",
            "215/215 [==============================] - 2s 9ms/step - loss: 0.3862 - accuracy: 0.8288 - val_loss: 0.4299 - val_accuracy: 0.8176\n"
          ]
        }
      ],
      "source": [
        "# Train a classifier on top of pretrained embeddings\n",
        "model_6_history = model_6.fit(train_sentences,\n",
        "                              train_labels,\n",
        "                              epochs=5,\n",
        "                              validation_data=(val_sentences, val_labels),\n",
        "                              callbacks=[create_tensorboard_callback(SAVE_DIR, \n",
        "                                                                     \"tf_hub_sentence_encoder\")])"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "KeI0kvVVDmbl"
      },
      "source": [
        "USE model trained! Let's make some predictions with it an evaluate them as we've done with our other models."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 86,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "xeyNXqU-gM2p",
        "outputId": "a37fc83d-67f5-4619-e750-1753ac5cf434"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "24/24 [==============================] - 1s 7ms/step\n"
          ]
        },
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "array([[0.14814094],\n",
              "       [0.74057853],\n",
              "       [0.9886474 ],\n",
              "       [0.22455953],\n",
              "       [0.7404941 ],\n",
              "       [0.6678845 ],\n",
              "       [0.98305696],\n",
              "       [0.9746391 ],\n",
              "       [0.923527  ],\n",
              "       [0.08624077]], dtype=float32)"
            ]
          },
          "metadata": {},
          "execution_count": 86
        }
      ],
      "source": [
        "# Make predictions with USE TF Hub model\n",
        "model_6_pred_probs = model_6.predict(val_sentences)\n",
        "model_6_pred_probs[:10]"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 87,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "Gbn1Z0FfgVdx",
        "outputId": "4cc50e0a-f3ec-4931-825b-86731bb0ec21"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "<tf.Tensor: shape=(10,), dtype=float32, numpy=array([0., 1., 1., 0., 1., 1., 1., 1., 1., 0.], dtype=float32)>"
            ]
          },
          "metadata": {},
          "execution_count": 87
        }
      ],
      "source": [
        "# Convert prediction probabilities to labels\n",
        "model_6_preds = tf.squeeze(tf.round(model_6_pred_probs))\n",
        "model_6_preds[:10]"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 88,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "N2Ow2de3okcb",
        "outputId": "c827c3bf-1c81-4910-894c-35e12e1b93bb"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "{'accuracy': 81.75853018372703,\n",
              " 'precision': 0.8206021490415145,\n",
              " 'recall': 0.8175853018372703,\n",
              " 'f1': 0.8158792847350168}"
            ]
          },
          "metadata": {},
          "execution_count": 88
        }
      ],
      "source": [
        "# Calculate model 6 performance metrics\n",
        "model_6_results = calculate_results(val_labels, model_6_preds)\n",
        "model_6_results"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 89,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "-BHnRHHHgp1r",
        "outputId": "0fe8251d-bad2-43bc-cb2a-aa43c2168d41"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Baseline accuracy: 79.27, New accuracy: 81.76, Difference: 2.49\n",
            "Baseline precision: 0.81, New precision: 0.82, Difference: 0.01\n",
            "Baseline recall: 0.79, New recall: 0.82, Difference: 0.02\n",
            "Baseline f1: 0.79, New f1: 0.82, Difference: 0.03\n"
          ]
        }
      ],
      "source": [
        "# Compare TF Hub model to baseline\n",
        "compare_baseline_to_new_results(baseline_results, model_6_results)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "LHwu4QjijYWG"
      },
      "source": [
        "### Model 7: TensorFlow Hub Pretrained Sentence Encoder 10% of the training data\n",
        "\n",
        "One of the benefits of using transfer learning methods, such as, the pretrained embeddings within the USE is the ability to get great results on a small amount of data (the USE paper even mentions this in the abstract).\n",
        "\n",
        "To put this to the test, we're going to make a small subset of the training data (10%), train a model and evaluate it."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 90,
      "metadata": {
        "id": "W5Sal8DpjzWm"
      },
      "outputs": [],
      "source": [
        "### NOTE: Making splits like this will lead to data leakage ###\n",
        "### (some of the training examples in the validation set) ###\n",
        "\n",
        "### WRONG WAY TO MAKE SPLITS (train_df_shuffled has already been split) ### \n",
        "\n",
        "# # Create subsets of 10% of the training data\n",
        "# train_10_percent = train_df_shuffled[[\"text\", \"target\"]].sample(frac=0.1, random_state=42)\n",
        "# train_sentences_10_percent = train_10_percent[\"text\"].to_list()\n",
        "# train_labels_10_percent = train_10_percent[\"target\"].to_list()\n",
        "# len(train_sentences_10_percent), len(train_labels_10_percent)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 91,
      "metadata": {
        "id": "XHgowC3GUPJH"
      },
      "outputs": [],
      "source": [
        "# One kind of correct way (there are more) to make data subset\n",
        "# (split the already split train_sentences/train_labels)\n",
        "train_sentences_90_percent, train_sentences_10_percent, train_labels_90_percent, train_labels_10_percent = train_test_split(np.array(train_sentences),\n",
        "                                                                                                                            train_labels,\n",
        "                                                                                                                            test_size=0.1,\n",
        "                                                                                                                            random_state=42)\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 92,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "j8jaydmiVnJP",
        "outputId": "62c3c080-1049-44af-98ee-bbe903de7971"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Total training examples: 6851\n",
            "Length of 10% training examples: 686\n"
          ]
        }
      ],
      "source": [
        "# Check length of 10 percent datasets\n",
        "print(f\"Total training examples: {len(train_sentences)}\")\n",
        "print(f\"Length of 10% training examples: {len(train_sentences_10_percent)}\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "7E2jr7rSEYT8"
      },
      "source": [
        "Because we've selected a random subset of the training samples, the classes should be roughly balanced (as they are in the full training dataset)."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 93,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "V0lEpFT0k0RB",
        "outputId": "7dcda766-f4ed-4f48-fad2-561c19a11b3d"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "0    415\n",
              "1    271\n",
              "dtype: int64"
            ]
          },
          "metadata": {},
          "execution_count": 93
        }
      ],
      "source": [
        "# Check the number of targets in our subset of data \n",
        "# (this should be close to the distribution of labels in the original train_labels)\n",
        "pd.Series(train_labels_10_percent).value_counts()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "ghl1qeGOEnXG"
      },
      "source": [
        "To make sure we're making an appropriate comparison between our model's ability to learn from the full training set and 10% subset, we'll clone our USE model (`model_6`) using the [`tf.keras.models.clone_model()`](https://www.tensorflow.org/api_docs/python/tf/keras/models/clone_model) method.\n",
        "\n",
        "Doing this will create the same architecture but reset the learned weights of the clone target (pretrained weights from the USE will remain but all others will be reset)."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 94,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "PGmxeAOBjdg2",
        "outputId": "b0979399-6bfe-43ae-e75c-7708bd730083"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Model: \"model_6_USE\"\n",
            "_________________________________________________________________\n",
            " Layer (type)                Output Shape              Param #   \n",
            "=================================================================\n",
            " USE (KerasLayer)            (None, 512)               256797824 \n",
            "                                                                 \n",
            " dense_5 (Dense)             (None, 64)                32832     \n",
            "                                                                 \n",
            " dense_6 (Dense)             (None, 1)                 65        \n",
            "                                                                 \n",
            "=================================================================\n",
            "Total params: 256,830,721\n",
            "Trainable params: 32,897\n",
            "Non-trainable params: 256,797,824\n",
            "_________________________________________________________________\n"
          ]
        }
      ],
      "source": [
        "# Clone model_6 but reset weights\n",
        "model_7 = tf.keras.models.clone_model(model_6)\n",
        "\n",
        "# Compile model\n",
        "model_7.compile(loss=\"binary_crossentropy\",\n",
        "                optimizer=tf.keras.optimizers.Adam(),\n",
        "                metrics=[\"accuracy\"])\n",
        "\n",
        "# Get a summary (will be same as model_6)\n",
        "model_7.summary()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "LxFkEM_aFoLK"
      },
      "source": [
        "Notice the layout of `model_7` is the same as `model_6`. Now let's train the newly created model on our 10% training data subset."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 95,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "LklU2maOkgUF",
        "outputId": "4b930b47-6c6f-48fc-99af-babc7a5eec2d"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Saving TensorBoard log files to: model_logs/10_percent_tf_hub_sentence_encoder/20230526-001758\n",
            "Epoch 1/5\n",
            "22/22 [==============================] - 4s 41ms/step - loss: 0.6671 - accuracy: 0.6997 - val_loss: 0.6443 - val_accuracy: 0.7415\n",
            "Epoch 2/5\n",
            "22/22 [==============================] - 0s 18ms/step - loss: 0.5895 - accuracy: 0.8309 - val_loss: 0.5846 - val_accuracy: 0.7467\n",
            "Epoch 3/5\n",
            "22/22 [==============================] - 0s 18ms/step - loss: 0.5116 - accuracy: 0.8382 - val_loss: 0.5336 - val_accuracy: 0.7677\n",
            "Epoch 4/5\n",
            "22/22 [==============================] - 0s 18ms/step - loss: 0.4492 - accuracy: 0.8411 - val_loss: 0.5040 - val_accuracy: 0.7703\n",
            "Epoch 5/5\n",
            "22/22 [==============================] - 0s 18ms/step - loss: 0.4080 - accuracy: 0.8469 - val_loss: 0.4880 - val_accuracy: 0.7703\n"
          ]
        }
      ],
      "source": [
        "# Fit the model to 10% of the training data\n",
        "model_7_history = model_7.fit(x=train_sentences_10_percent,\n",
        "                              y=train_labels_10_percent,\n",
        "                              epochs=5,\n",
        "                              validation_data=(val_sentences, val_labels),\n",
        "                              callbacks=[create_tensorboard_callback(SAVE_DIR, \"10_percent_tf_hub_sentence_encoder\")])"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "9Qpyqdh-F6Eh"
      },
      "source": [
        "Due to the smaller amount of training data, training happens even quicker than before.\n",
        "\n",
        "Let's evaluate our model's performance after learning on 10% of the training data."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 96,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "ot6MRnznlgCL",
        "outputId": "0ad5f3fd-ee51-47ab-e338-6091e9a1fab7"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "24/24 [==============================] - 1s 7ms/step\n"
          ]
        },
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "array([[0.24178001],\n",
              "       [0.8116845 ],\n",
              "       [0.91511923],\n",
              "       [0.32094172],\n",
              "       [0.587357  ],\n",
              "       [0.82938445],\n",
              "       [0.8401675 ],\n",
              "       [0.8496708 ],\n",
              "       [0.8371127 ],\n",
              "       [0.14010696]], dtype=float32)"
            ]
          },
          "metadata": {},
          "execution_count": 96
        }
      ],
      "source": [
        "# Make predictions with the model trained on 10% of the data\n",
        "model_7_pred_probs = model_7.predict(val_sentences)\n",
        "model_7_pred_probs[:10]"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 97,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "Vj_4aZellpRu",
        "outputId": "996222f8-c15f-4468-fbf2-95b5cbdc5d38"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "<tf.Tensor: shape=(10,), dtype=float32, numpy=array([0., 1., 1., 0., 1., 1., 1., 1., 1., 0.], dtype=float32)>"
            ]
          },
          "metadata": {},
          "execution_count": 97
        }
      ],
      "source": [
        "# Convert prediction probabilities to labels\n",
        "model_7_preds = tf.squeeze(tf.round(model_7_pred_probs))\n",
        "model_7_preds[:10]"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 98,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "T_lTXrDblyva",
        "outputId": "85896126-668c-4197-d4d2-6375b6b9dfb0"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "{'accuracy': 77.03412073490814,\n",
              " 'precision': 0.7760118694840564,\n",
              " 'recall': 0.7703412073490814,\n",
              " 'f1': 0.7665375100103654}"
            ]
          },
          "metadata": {},
          "execution_count": 98
        }
      ],
      "source": [
        "# Calculate model results\n",
        "model_7_results = calculate_results(val_labels, model_7_preds)\n",
        "model_7_results"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 99,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "G84ezltll6DT",
        "outputId": "dd9871c4-16fc-4281-91fc-70504128e8f6"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Baseline accuracy: 79.27, New accuracy: 77.03, Difference: -2.23\n",
            "Baseline precision: 0.81, New precision: 0.78, Difference: -0.04\n",
            "Baseline recall: 0.79, New recall: 0.77, Difference: -0.02\n",
            "Baseline f1: 0.79, New f1: 0.77, Difference: -0.02\n"
          ]
        }
      ],
      "source": [
        "# Compare to baseline\n",
        "compare_baseline_to_new_results(baseline_results, model_7_results)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "iBs9V61EGh0J"
      },
      "source": [
        "## Comparing the performance of each of our models\n",
        "\n",
        "Woah. We've come a long way! From training a baseline to several deep models.\n",
        "\n",
        "Now it's time to compare our model's results.\n",
        "\n",
        "But just before we do, it's worthwhile mentioning, this type of practice is a standard deep learning workflow. Training various different models, then comparing them to see which one performed best and continuing to train it if necessary.\n",
        "\n",
        "The important thing to note is that for all of our modelling experiments we used the same training data (except for `model_7` where we used 10% of the training data).\n",
        "\n",
        "To visualize our model's performances, let's create a pandas DataFrame we our results dictionaries and then plot it."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 100,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 300
        },
        "id": "Ex0NSaz7lRf-",
        "outputId": "fb5f5272-6b73-4ee0-e04d-0e01f256ceda"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "                          accuracy  precision    recall        f1\n",
              "baseline                 79.265092   0.811139  0.792651  0.786219\n",
              "simple_dense             78.608924   0.790328  0.786089  0.783297\n",
              "lstm                     75.590551   0.756716  0.755906  0.753960\n",
              "gru                      77.559055   0.776327  0.775591  0.774090\n",
              "bidirectional            76.771654   0.767545  0.767717  0.766793\n",
              "conv1d                   78.740157   0.790061  0.787402  0.785228\n",
              "tf_hub_sentence_encoder  81.758530   0.820602  0.817585  0.815879\n",
              "tf_hub_10_percent_data   77.034121   0.776012  0.770341  0.766538"
            ],
            "text/html": [
              "\n",
              "  <div id=\"df-4bf967fe-ed48-4e35-a870-cc6d18c95c35\">\n",
              "    <div class=\"colab-df-container\">\n",
              "      <div>\n",
              "<style scoped>\n",
              "    .dataframe tbody tr th:only-of-type {\n",
              "        vertical-align: middle;\n",
              "    }\n",
              "\n",
              "    .dataframe tbody tr th {\n",
              "        vertical-align: top;\n",
              "    }\n",
              "\n",
              "    .dataframe thead th {\n",
              "        text-align: right;\n",
              "    }\n",
              "</style>\n",
              "<table border=\"1\" class=\"dataframe\">\n",
              "  <thead>\n",
              "    <tr style=\"text-align: right;\">\n",
              "      <th></th>\n",
              "      <th>accuracy</th>\n",
              "      <th>precision</th>\n",
              "      <th>recall</th>\n",
              "      <th>f1</th>\n",
              "    </tr>\n",
              "  </thead>\n",
              "  <tbody>\n",
              "    <tr>\n",
              "      <th>baseline</th>\n",
              "      <td>79.265092</td>\n",
              "      <td>0.811139</td>\n",
              "      <td>0.792651</td>\n",
              "      <td>0.786219</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>simple_dense</th>\n",
              "      <td>78.608924</td>\n",
              "      <td>0.790328</td>\n",
              "      <td>0.786089</td>\n",
              "      <td>0.783297</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>lstm</th>\n",
              "      <td>75.590551</td>\n",
              "      <td>0.756716</td>\n",
              "      <td>0.755906</td>\n",
              "      <td>0.753960</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>gru</th>\n",
              "      <td>77.559055</td>\n",
              "      <td>0.776327</td>\n",
              "      <td>0.775591</td>\n",
              "      <td>0.774090</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>bidirectional</th>\n",
              "      <td>76.771654</td>\n",
              "      <td>0.767545</td>\n",
              "      <td>0.767717</td>\n",
              "      <td>0.766793</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>conv1d</th>\n",
              "      <td>78.740157</td>\n",
              "      <td>0.790061</td>\n",
              "      <td>0.787402</td>\n",
              "      <td>0.785228</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>tf_hub_sentence_encoder</th>\n",
              "      <td>81.758530</td>\n",
              "      <td>0.820602</td>\n",
              "      <td>0.817585</td>\n",
              "      <td>0.815879</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>tf_hub_10_percent_data</th>\n",
              "      <td>77.034121</td>\n",
              "      <td>0.776012</td>\n",
              "      <td>0.770341</td>\n",
              "      <td>0.766538</td>\n",
              "    </tr>\n",
              "  </tbody>\n",
              "</table>\n",
              "</div>\n",
              "      <button class=\"colab-df-convert\" onclick=\"convertToInteractive('df-4bf967fe-ed48-4e35-a870-cc6d18c95c35')\"\n",
              "              title=\"Convert this dataframe to an interactive table.\"\n",
              "              style=\"display:none;\">\n",
              "        \n",
              "  <svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\"viewBox=\"0 0 24 24\"\n",
              "       width=\"24px\">\n",
              "    <path d=\"M0 0h24v24H0V0z\" fill=\"none\"/>\n",
              "    <path d=\"M18.56 5.44l.94 2.06.94-2.06 2.06-.94-2.06-.94-.94-2.06-.94 2.06-2.06.94zm-11 1L8.5 8.5l.94-2.06 2.06-.94-2.06-.94L8.5 2.5l-.94 2.06-2.06.94zm10 10l.94 2.06.94-2.06 2.06-.94-2.06-.94-.94-2.06-.94 2.06-2.06.94z\"/><path d=\"M17.41 7.96l-1.37-1.37c-.4-.4-.92-.59-1.43-.59-.52 0-1.04.2-1.43.59L10.3 9.45l-7.72 7.72c-.78.78-.78 2.05 0 2.83L4 21.41c.39.39.9.59 1.41.59.51 0 1.02-.2 1.41-.59l7.78-7.78 2.81-2.81c.8-.78.8-2.07 0-2.86zM5.41 20L4 18.59l7.72-7.72 1.47 1.35L5.41 20z\"/>\n",
              "  </svg>\n",
              "      </button>\n",
              "      \n",
              "  <style>\n",
              "    .colab-df-container {\n",
              "      display:flex;\n",
              "      flex-wrap:wrap;\n",
              "      gap: 12px;\n",
              "    }\n",
              "\n",
              "    .colab-df-convert {\n",
              "      background-color: #E8F0FE;\n",
              "      border: none;\n",
              "      border-radius: 50%;\n",
              "      cursor: pointer;\n",
              "      display: none;\n",
              "      fill: #1967D2;\n",
              "      height: 32px;\n",
              "      padding: 0 0 0 0;\n",
              "      width: 32px;\n",
              "    }\n",
              "\n",
              "    .colab-df-convert:hover {\n",
              "      background-color: #E2EBFA;\n",
              "      box-shadow: 0px 1px 2px rgba(60, 64, 67, 0.3), 0px 1px 3px 1px rgba(60, 64, 67, 0.15);\n",
              "      fill: #174EA6;\n",
              "    }\n",
              "\n",
              "    [theme=dark] .colab-df-convert {\n",
              "      background-color: #3B4455;\n",
              "      fill: #D2E3FC;\n",
              "    }\n",
              "\n",
              "    [theme=dark] .colab-df-convert:hover {\n",
              "      background-color: #434B5C;\n",
              "      box-shadow: 0px 1px 3px 1px rgba(0, 0, 0, 0.15);\n",
              "      filter: drop-shadow(0px 1px 2px rgba(0, 0, 0, 0.3));\n",
              "      fill: #FFFFFF;\n",
              "    }\n",
              "  </style>\n",
              "\n",
              "      <script>\n",
              "        const buttonEl =\n",
              "          document.querySelector('#df-4bf967fe-ed48-4e35-a870-cc6d18c95c35 button.colab-df-convert');\n",
              "        buttonEl.style.display =\n",
              "          google.colab.kernel.accessAllowed ? 'block' : 'none';\n",
              "\n",
              "        async function convertToInteractive(key) {\n",
              "          const element = document.querySelector('#df-4bf967fe-ed48-4e35-a870-cc6d18c95c35');\n",
              "          const dataTable =\n",
              "            await google.colab.kernel.invokeFunction('convertToInteractive',\n",
              "                                                     [key], {});\n",
              "          if (!dataTable) return;\n",
              "\n",
              "          const docLinkHtml = 'Like what you see? Visit the ' +\n",
              "            '<a target=\"_blank\" href=https://colab.research.google.com/notebooks/data_table.ipynb>data table notebook</a>'\n",
              "            + ' to learn more about interactive tables.';\n",
              "          element.innerHTML = '';\n",
              "          dataTable['output_type'] = 'display_data';\n",
              "          await google.colab.output.renderOutput(dataTable, element);\n",
              "          const docLink = document.createElement('div');\n",
              "          docLink.innerHTML = docLinkHtml;\n",
              "          element.appendChild(docLink);\n",
              "        }\n",
              "      </script>\n",
              "    </div>\n",
              "  </div>\n",
              "  "
            ]
          },
          "metadata": {},
          "execution_count": 100
        }
      ],
      "source": [
        "# Combine model results into a DataFrame\n",
        "all_model_results = pd.DataFrame({\"baseline\": baseline_results,\n",
        "                                  \"simple_dense\": model_1_results,\n",
        "                                  \"lstm\": model_2_results,\n",
        "                                  \"gru\": model_3_results,\n",
        "                                  \"bidirectional\": model_4_results,\n",
        "                                  \"conv1d\": model_5_results,\n",
        "                                  \"tf_hub_sentence_encoder\": model_6_results,\n",
        "                                  \"tf_hub_10_percent_data\": model_7_results})\n",
        "all_model_results = all_model_results.transpose()\n",
        "all_model_results"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 101,
      "metadata": {
        "id": "v-s2DSLpmM1F"
      },
      "outputs": [],
      "source": [
        "# Reduce the accuracy to same scale as other metrics\n",
        "all_model_results[\"accuracy\"] = all_model_results[\"accuracy\"]/100"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 102,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 763
        },
        "id": "Wp69bR8umD5g",
        "outputId": "cf55f16b-b9ac-4dba-9e29-9a44e27df2d4"
      },
      "outputs": [
        {
          "output_type": "display_data",
          "data": {
            "text/plain": [
              "<Figure size 1000x700 with 1 Axes>"
            ],
            "image/png": "iVBORw0KGgoAAAANSUhEUgAAA7IAAALqCAYAAAAIKmjaAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAABwl0lEQVR4nO3deVxU9eL/8feAAqKAOy4XxS2VRFFwS3NJUq99Lc1uphZKaYthKlrmTbHMRCuXTK/kdtXK1Mqsm12zS1qKpLmAVu4bbiBqQriAAr8//DX3TqA5yHA8M6/n4zGPmM/5zMwbxoA355zPseTn5+cLAAAAAACTcDM6AAAAAAAA9qDIAgAAAABMhSILAAAAADAViiwAAAAAwFQosgAAAAAAU6HIAgAAAABMhSILAAAAADAViiwAAAAAwFRKGR3gVuTl5enUqVPy8fGRxWIxOg4AAAAAg+Tn5+u3335TjRo15ObGfjlXZYoie+rUKQUEBBgdAwAAAMAd4vjx4/rLX/5idAwYxBRF1sfHR9L1f6y+vr4GpwEAAABglMzMTAUEBFg7AlyTKYrs74cT+/r6UmQBAAAAcMqhi+OgcgAAAACAqVBkAQAAAACmQpEFAAAAAJiKKc6RBQAAAIBblZubq6tXrxodA3Zyd3dXqVKlbun8Z4osAAAAAKeRlZWlEydOKD8/3+goKAJvb29Vr15dHh4eN51HkQUAAADgFHJzc3XixAl5e3urSpUqrGxsIvn5+crJyVF6erqOHDmiBg0ayM3txmfCUmQBAAAAOIWrV68qPz9fVapUUZkyZYyOAzuVKVNGpUuX1rFjx5STkyMvL68bzmWxJwAAAABOhT2x5nWzvbA28xycAwAAAACAYkWRBQAAAACYCufIAgAAAHBqgS+vKdHXOzrlgRJ9PVfEHlkAAAAAgI07/Tq8FFkAAAAAMNjatWvVvn17lS9fXpUqVdL//d//6dChQ9btJ06cUL9+/VSxYkWVLVtWYWFh2rJli3X7v/71L7Vs2VJeXl6qXLmyevfubd1msVi0evVqm9crX768Fi9eLEk6evSoLBaLVqxYoY4dO8rLy0sffvihzp07p379+qlmzZry9vZWcHCwPvroI5vnycvL05tvvqn69evL09NTtWrV0htvvCFJuu+++xQVFWUzPz09XR4eHoqPj7+trxdFFgAAAAAMdvHiRUVHR2vbtm2Kj4+Xm5ubevfurby8PGVlZaljx446efKkvvjiCyUnJ+ull15SXl6eJGnNmjXq3bu3evTooZ07dyo+Pl6tWrWyO8PLL7+s4cOHa8+ePerWrZuuXLmi0NBQrVmzRj/99JOefvppPfHEE9q6dav1MWPHjtWUKVM0fvx4/fLLL1q2bJn8/f0lSYMHD9ayZcuUnZ1tnf/BBx+oZs2auu+++27r68U5sgAAAABgsD59+tjcX7RokapUqaJffvlFmzdvVnp6un788UdVrFhRklS/fn3r3DfeeEOPPfaYXnvtNetYs2bN7M4wYsQIPfzwwzZjo0ePtn48bNgwff3111q5cqVatWql3377Te+8845mz56tgQMHSpLq1aun9u3bS5IefvhhRUVF6fPPP9ejjz4qSVq8eLEGDRp025dIYo8sAAAAABjswIED6tevn+rWrStfX18FBgZKklJSUpSUlKTmzZtbS+wfJSUlqUuXLredISwszOZ+bm6uXn/9dQUHB6tixYoqV66cvv76a6WkpEiS9uzZo+zs7Bu+tpeXl5544gktWrRIkrRjxw799NNPGjRo0G1nZY8sAAAAABisZ8+eql27tubPn68aNWooLy9PTZo0UU5OjsqUKXPTx/7ZdovFovz8fJuxwhZzKlu2rM39t956S++8845mzpyp4OBglS1bViNGjFBOTs4tva50/fDikJAQnThxQv/85z913333qXbt2n/6uD/DHlkAAAAAMNC5c+e0b98+jRs3Tl26dFHjxo3166+/Wrc3bdpUSUlJOn/+fKGPb9q06U0XT6pSpYpOnz5tvX/gwAFdunTpT3MlJCTooYce0uOPP65mzZqpbt262r9/v3V7gwYNVKZMmZu+dnBwsMLCwjR//nwtW7ZMTz755J++7q2gyAIAAACAgSpUqKBKlSpp3rx5OnjwoL799ltFR0dbt/fr10/VqlVTr169lJCQoMOHD+vTTz9VYmKiJGnChAn66KOPNGHCBO3Zs0e7d+/W1KlTrY+/7777NHv2bO3cuVPbtm3Ts88+q9KlS/9prgYNGuibb77R5s2btWfPHj3zzDNKS0uzbvfy8tKYMWP00ksvaenSpTp06JB++OEHLVy40OZ5Bg8erClTpig/P99mNeXbQZEFAAAAAAO5ublp+fLl2r59u5o0aaKRI0fqrbfesm738PDQunXr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y0uXL182OJ1raNmypaKjo/X+++/L399fkpSWlqYXX3xRrVq1MjgdAGfn5+cn6fq1G318fGwW+vPw8FCbNm00ZMgQo+K5jE8//VSffPKJzR+Re/TooTJlyujRRx+lyJaQkJAQffjhh0bHcFlLly5V3759bf6gJkk5OTlavny5IiIiJEnvvfee9Xcm4E7HocUOMmDAAO3du1fNmzfXRx99pJSUFFWqVElffPGF/v73v+unn34yOqLTO3jwoHr37q39+/dbr4d2/PhxNWjQQKtXr7buMQcAR3rttdf04osvcgirQby9vbV9+/YCK+b+/PPPatWqlS5evGhQMteSl5engwcP6syZM9ZDvH/HgmeO5+7urtOnTxe4/OO5c+dUtWpVFsCEKVFkHeTChQsaN26cjh8/rueee07du3eXJE2YMEEeHh565ZVXDE7oGvLz8/XNN99o7969kqTGjRsrPDzcunIxADjakSNHdO3atQKHUB44cEClS5dWYGCgMcFcRJcuXVSpUiUtXbpUXl5ekqTLly9r4MCBOn/+vP7zn/8YnND5/fDDD+rfv7+OHTtW6JUEKFGOd6Nz9ZOTk2+4IB1wp6PIwuUFBwfrq6++su61BYDi1LFjRz355JMaOHCgzfgHH3ygBQsWaMOGDcYEcxG7d+9W9+7dlZ2drWbNmkm6/su7p6en1q1bp7vvvtvghM4vJCREd911l1577TVVr169wB+Tfz8MH8WvefPmslgsSk5O1t13321zGbDc3FwdOXJE3bt318qVKw1MCRQNRdbBLl26pJSUFOXk5NiMN23a1KBE+CNW6APgSL6+vtqxY0eB0xkOHjyosLAwXbhwwZhgLuTSpUv68MMPbY7OGTBggM15y3CcsmXLKjk5mVN6DPDaa69Z/ztq1CiVK1fOus3Dw0OBgYHq06ePPDw8jIoIFBmLPTlIenq6Bg0adMPrlXIYDQC4BovFot9++63AeEZGBj8LSkBsbKz8/f0LLKy1aNEipaena8yYMQYlcx2tW7fWwYMHKbIGmDBhgiQpMDBQffv2tR5eDzgDN6MDOKsRI0YoIyNDW7ZsUZkyZbR27VotWbJEDRo00BdffGF0PABACenQoYNiY2NtSmtubq5iY2MLXOcaxe+9995To0aNCozffffdiouLMyCR6xk2bJhGjRqlxYsXa/v27dq1a5fNDY43cOBAeXl5KScnRydOnFBKSorNDTAjDi12kOrVq+vzzz9Xq1at5Ovrq23btumuu+7SF198oTfffFObNm0yOiL+Pw4tBuBIv/zyizp06KDy5cvr3nvvlSRt3LhRmZmZ+vbbb9WkSRODEzo3Ly8v7dmzR3Xq1LEZP3z4sIKCgnTlyhWDkrkON7eC+00sFovy8/NZ7KmEHDhwQE8++aQ2b95sM857ADPj0GIHuXjxonWJ8woVKig9PV133XWXgoODtWPHDoPTAQBKSlBQkHbt2qXZs2crOTlZZcqUUUREhKKiolSxYkWj4zm9gIAAJSQkFCiyCQkJqlGjhkGpXMuRI0eMjuDyBg0apFKlSunLL78sdMEtwIwosg7SsGFD7du3T4GBgWrWrJnee+89BQYGKi4uTtWrVzc6HgCgBNWoUUOTJ082OoZLGjJkiEaMGKGrV6/qvvvukyTFx8frpZde0qhRowxO5xpq165tdASXl5SUpO3btxd6mD1gVhRZBxk+fLhOnz4t6fqJ9t27d9cHH3wgDw8PLVmyxOB0rufKlSs3XODgvffek7+/fwknAuBKNm7cqPfee0+HDx/Wxx9/rJo1a+r9999XnTp1OE/WwV588UWdO3dOQ4cOtV5BwMvLS2PGjNHYsWMNTuc63n//fcXFxenIkSNKTExU7dq1NXPmTNWpU0cPPfSQ0fGcXlBQkM6ePWt0DKBYsdiTgzz++OMaNGiQJKlFixY6duyYtm3bphMnTqhv377GhnMReXl5ev3111WzZk2VK1dOhw8fliSNHz9eCxcutM7r37+/ypYta1RMAE7u008/Vbdu3VSmTBnt2LFD2dnZkq6vWsxeWsezWCyaOnWq0tPT9cMPPyg5OVnnz59XTEyM0dFcxty5cxUdHa0ePXrowoUL1vMxy5cvr5kzZxobzkVMnTpVL730kjZs2KBz584pMzPT5gaYEUXWgRYuXKgmTZrIy8tLFSpUUEREhFavXm10LJcxadIkLV68WG+++abN9dGaNGmiBQsWGJgMgCuZNGmS4uLiNH/+fJUuXdo63q5dO9ZMKEHlypVTy5Yt1aRJE3l6ehodx6W8++67mj9/vl555RW5u7tbx8PCwrR7924Dk7mO8PBw/fDDD+rSpYuqVq2qChUqqEKFCipfvrwqVKhgdDygSDi02EFiYmI0ffp0DRs2TG3btpUkJSYmauTIkUpJSdHEiRMNTuj8li5dqnnz5qlLly569tlnrePNmjXT3r17DUwGwJXs27dPHTp0KDDu5+enCxculHwgoIQdOXJEzZs3LzDu6empixcvGpDI9axfv97oCECxo8g6yNy5czV//nz169fPOvbggw+qadOmGjZsGEW2BJw8ebLQi6/n5eXp6tWrBiQC4IqqVaumgwcPKjAw0GZ806ZNXPYLLqFOnTpKSkoqsOjT2rVr1bhxY4NSuZaOHTsaHQEodhxa7CBXr15VWFhYgfHQ0FBdu3bNgESuJygoSBs3biww/sknnxT6l2EAcIQhQ4Zo+PDh2rJliywWi06dOqUPP/xQo0eP1nPPPWd0PMDhoqOj9fzzz2vFihXKz8/X1q1b9cYbb2js2LF66aWXjI7nMjZu3KjHH39c99xzj06ePCnp+iJcmzZtMjgZUDTskXWQJ554QnPnztX06dNtxufNm6cBAwYYlMq1xMTEaODAgTp58qTy8vK0atUq7du3T0uXLtWXX35pdDwALuLll19WXl6eunTpokuXLqlDhw7y9PTU6NGjNWzYMKPjAQ43ePBglSlTRuPGjdOlS5fUv39/1ahRQ++8844ee+wxo+O5hE8//VRPPPGEBgwYUOiic1999ZXBCQH7WfLz8/ONDuEsoqOjrR9fu3ZNixcvVq1atdSmTRtJ0pYtW5SSkqKIiAi9++67RsV0KRs3btTEiROVnJysrKwstWjRQjExMeratavR0QC4gNzcXCUkJKhp06by9vbWwYMHlZWVpaCgIJUrV87oeECJu3TpkrKyslS1atUC2xISEhQWFsZiXA7QvHlzjRw5UhEREfLx8VFycrLq1q2rnTt36q9//atSU1ONjgjYjSJbjDp37nxL8ywWi7799lsHpwEA3Am8vLy0Z88e1alTx+gowB3N19dXSUlJnDvuAN7e3vrll18UGBhoU2QPHz6soKAgXblyxeiIgN04tLgYsSIcAOCPmjRposOHD1NkgT/BvhXHYdE5OCOKLJxKhQoVZLFYbmnu+fPnHZwGAK5fR3b06NF6/fXXFRoaqrJly9ps9/X1NSgZAFfx+6JzixYtsi46l5iYqNGjR2v8+PFGxwOKhCILpzJz5kyjIwCAjR49eki6fgm2//1DW35+viwWi3Jzc42KBsBFsOgcnBHnyAIA4EDffffdTbdzfUfguv89dxOOkZOTw6JzcBoUWTi13NxcffbZZ9qzZ4+k69eWfeihh1SqFAcjAABwJ2GxJ8fJyMhQbm6uKlasaDN+/vx5lSpVilMcYEr8Ng+n9fPPP+vBBx9UamqqGjZsKEmaOnWqqlSpon/9619q0qSJwQkBOKtdu3apSZMmcnNz065du246t2nTpiWUCrizsW/FcR577DH17NlTQ4cOtRlfuXKlvvjiC64jC1NijyycVtu2bVWlShUtWbJEFSpUkCT9+uuvGjRokNLT07V582aDEwJwVm5ubkpNTVXVqlXl5uYmi8VS6C/pnCMLV3Ht2jVt2LBBhw4dUv/+/eXj46NTp07J19eXw1tLQMWKFZWQkKDGjRvbjO/du1ft2rXTuXPnDEoGFB17ZOG0kpKStG3bNmuJla6vavzGG2+oZcuWBiYD4OyOHDmiKlWqWD8GXNmxY8fUvXt3paSkKDs7W/fff798fHw0depUZWdnKy4uzuiITi87O1vXrl0rMH716lVdvnzZgETA7XMzOgDgKHfddZfS0tIKjJ85c0b169c3IBEAV1G7dm3rCsW1a9e+6Q1wdsOHD1dYWJh+/fVXlSlTxjreu3dvxcfHG5jMdbRq1Urz5s0rMB4XF6fQ0FADEgG3jz2ycFqxsbF64YUX9Oqrr6pNmzaSpB9++EETJ07U1KlTlZmZaZ3LIgcAitMXX3xxy3MffPBBByYBjLdx40Zt3rxZHh4eNuOBgYE6efKkQalcy6RJkxQeHq7k5GR16dJFkhQfH68ff/xR69atMzgdUDScIwun5eb23wMOft8z8vs/9/+9zzlqAIrb/37/kVTgHNn/vZ4s33/g7CpUqKCEhAQFBQXZXGJn06ZN6tOnT6FHT6H4JScn680331RSUpLKlCmjpk2bauzYsWrQoIHR0YAiYY8snNb69euNjgDAReXl5Vk//s9//qMxY8Zo8uTJatu2rSQpMTFR48aN0+TJk42KCJSYrl27aubMmdZDWy0Wi7KysjRhwgT16NHD4HTO7+rVq3rmmWc0fvx4ffjhh0bHAYoNe2QBAHCgJk2aKC4uTu3bt7cZ37hxo55++mnrda4BZ3XixAl169ZN+fn5OnDggMLCwnTgwAFVrlxZ33//vapWrWp0RKfn5+enpKQk1alTx+goQLGhyMKpXblyRbt27dKZM2ds9pBInJcGoGSUKVNGP/74Y4FrV+/atUutW7dmxVC4hGvXrmnFihVKTk5WVlaWWrRooQEDBtgs/gTHGThwoEJCQjRy5EijowDFhiILp7V27VpFRETo7NmzBbZxXiyAktKhQwd5eXnp/fffl7+/vyQpLS1NERERunLlir777juDEwJwdpMmTdK0adPUpUsXhYaGqmzZsjbbX3jhBYOSAUVHkYXTatCggbp27aqYmBjrL48AUNIOHjyo3r17a//+/QoICJAkHT9+XA0aNNDq1au5HBicXmxsrPz9/fXkk0/ajC9atEjp6ekaM2aMQclcx80OKbZYLDp8+HAJpgGKB0UWTsvX11c7d+5UvXr1jI4CwMXl5+frm2++0d69eyVJjRs3Vnh4uM3qxYCzCgwM1LJly3TPPffYjG/ZskWPPfaYjhw5YlAyAGbGqsVwWo888og2bNhAkQVgOIvFoq5du6pr165GRwFKXGpqqqpXr15gvEqVKjp9+rQBiVxXTk6Ojhw5onr16qlUKWoAzI1/wXBas2fP1t/+9jdt3LhRwcHBKl26tM12zgcB4CizZs3S008/LS8vL82aNeumc/leBGcXEBCghISEAoe3JiQkqEaNGgalci2XLl3SsGHDtGTJEknS/v37VbduXQ0bNkw1a9bUyy+/bHBCwH4cWgyntXDhQj377LPy8vJSpUqVbA7h43wQAI5Up04dbdu2TZUqVeLcNLi8N998U2+++abeeust3XfffZKk+Ph4vfTSSxo1apTGjh1rcELnN3z4cCUkJGjmzJnq3r27du3apbp16+rzzz/Xq6++qp07dxodEbAbRRZOq1q1anrhhRf08ssvy83Nzeg4AKDff+RybixcSX5+vl5++WXNmjVLOTk5kiQvLy+NGTNGMTExBqdzDbVr19aKFSvUpk0b+fj4KDk5WXXr1tXBgwfVokULZWZmGh0RsBu/3cNp5eTkqG/fvpRYAIZbuHChmjRpIi8vL3l5ealJkyZasGCB0bGAEmGxWDR16lSlp6frhx9+UHJyss6fP0+JLUHp6emqWrVqgfGLFy/yhzWYFr/hw2kNHDhQK1asMDoGABcXExOj4cOHq2fPnvr444/18ccfq2fPnho5ciS/yMOllCtXTi1btlSTJk3k6elpdByXEhYWpjVr1ljv/15eFyxYoLZt2xoVC7gtHFoMp/XCCy9o6dKlatasmZo2bVpgsafp06cb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\n"
          },
          "metadata": {}
        }
      ],
      "source": [
        "# Plot and compare all of the model results\n",
        "all_model_results.plot(kind=\"bar\", figsize=(10, 7)).legend(bbox_to_anchor=(1.0, 1.0));"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "avbdkiIuKNNr"
      },
      "source": [
        "Looks like our pretrained USE TensorFlow Hub models have the best performance, even the one with only 10% of the training data seems to outperform the other models. This goes to show the power of transfer learning.\n",
        "\n",
        "How about we drill down and get the F1-score's of each model?"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 103,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 763
        },
        "id": "yktdOiufmm3p",
        "outputId": "79b87a0a-2d2a-4621-920d-58f7b1d65491"
      },
      "outputs": [
        {
          "output_type": "display_data",
          "data": {
            "text/plain": [
              "<Figure size 1000x700 with 1 Axes>"
            ],
            "image/png": "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tXTPPffo2WefrbBIB9ynpKRE27dv19GjRxUaGsqqgzXkiy++cPnYbrerSZMmat++vWrVYvZ4TXnwwQe1cuVKPf3005XeDM89IjVrz549ysjIUPv27dWlSxer43iM/Px83XrrrVq9erX8/PxUu3Ztl+MmLspB0XGT1q1ba8mSJbr88svl7++v7OxstW3bVtu3b1f37t25VF5DSktLtXjxYm3ZskVHjx5V9+7dNXz4cNWpU8fqaB6lqKhIO3bskCS1a9eOgmOhwsJCff755+rQoYMuvfRSq+MY7eTJk/rb3/6mJ554gj3VLNaqVSvnzfABAQHKzMxU+/bt9frrr+uf//wny6y72aJFixQdHS0fHx+X8ZKSEi1evFgxMTEWJfMsUVFRysnJ0T333KOgoKAKixGYeF8bRcdN/Pz89M0336ht27YuRSc7O1t9+vRRQUGB1REBeIBbb71Vffr0UXx8vI4fP66wsDDt2rVLDodDixcv1tChQ62OaDQ2j74w1KtXT999951atWqliy66SO+//7569uypXbt2qXPnzjp69KjVEY3m5eWln376qcJMip9//llNmzZlH50a4ufnp/T09EpXgzQVixG4SUREhJYuXer8+HRrnjt3riIjI62K5XG2bdum2bNna8qUKZo8ebLLA9bZsWOHrrnmGqtjeITVq1c7l/L+4IMPVF5eriNHjmjmzJnso1MDTm8eDWtxM7y1HA5HpUsZ79u3T4GBgRYk8kwdO3bU8ePHrY5Ro5gg7CZTp07VX/7yF3333XcqLS3Viy++qO+++07r16+vMGcb7jFnzhzdf//9aty4sZo1a+byj6zNZlNiYqKF6Tzb0aNH+XtQQwoKCtSwYUNJ0vLlyzV06FD5+flp8ODBevTRRy1OZz42j74wxMbGKjs7W3379tX48eN1/fXX6+WXX9bJkyeVnJxsdTxjdevWTTabTTabTf3793e5L62srEy7du3SoEGDLEzoWaZNm6ZHHnlEzzzzjDp37lzhHp2AgACLkrkPU9fcaMeOHZo2bZqys7Od94eMGzdOnTt3tjqaR2jdurUeeOABjRs3zuooHmfmzJlnPb5//34999xzTFeoAZdccommTJmiwYMHq02bNlq8eLGuueYaZWdnq3///jp06JDVEY12tilrNptNO3furME0OI2b4WvGU0895fzvI4884rIIire3t0JCQjR06NAK+93BPez2UxO5/vvq2ukrbiZ+T6bowFgBAQHKyspiU0QL2O12NW/e/IzfvEpKSpSbm2vkP6oXmn/84x8aPXq06tWrp9atWyszM1N2u10vvfSS3n//fa1cudLqiAAMt3DhQkVHR8vX19fqKB7tj2ZS9O3bt4aS1ByKTjWqykpqJl4evNDcc8896tGjh+677z6ro3icNm3aaPr06br11lsrPZ6VlaXw8HCKTg3ZtGmT9u7dq2uvvdb5jurSpUtVv359j9oh2woJCQmVjttsNvn6+qp9+/a68cYbndML4T5paWlKS0vTwYMHVV5e7nJs/vz5FqUCak5OTo6Cg4MrvaKzd+9etWrVyqJk7kPRqUZ2u73Sm+0qww947peUlKTk5GQNHjy40rmozI13n1tuuUXt2rXT9OnTKz2enZ2tbt26VfhhAzBNv379lJmZqbKyMnXo0EGS9OOPP8rLy0sdO3bU1q1bZbPZtHbtWoWGhlqc1lxPPfWUJk+erIiICDVv3rzC9+oPPvjAomSeoaysTC+88ILefvtt5eTkqKSkxOW4ifu3XIg8cfU7ik41+v0lwd27d2v8+PG66667nKuspaena+HChUpKSjJyrfILDXPjrfPdd9/p2LFjioiIqPT4yZMndeDAAbVu3bqGk3mesrIyLViw4IzvZH/++ecWJfMMKSkpWrNmjV577TXnlfyCggKNHDlSV155peLi4jRs2DAdP35c//nPfyxOa67mzZtrxowZuvPOO62O4pESExM1d+5cPfLII5o4caIef/xx7d69Wx9++KESExN547GG2O125eXlqUmTJi7je/bsUWhoqIqKiixK5j4UHTfp37+/Ro4cqdtvv91l/K233tLs2bO1atUqa4IB8Cjx8fFasGCBBg8eXOk72S+88IJFyTxDy5Yt9emnn1a4WvPtt99qwIAB2r9/vzIzMzVgwAAWhnCjRo0aaePGjWrXrp3VUTxSu3btNHPmTA0ePFj+/v7Kyspyjn355Zd66623rI5otNNTaF988UXFxcXJz8/PeaysrEwbNmyQl5eX1q1bZ1VEt2F5aTdJT09XampqhfGIiAiNHDnSgkRAzZsyZYqGDx/OZokWWrx4sd5++21dd911VkfxSAUFBTp48GCFopOfn++8r7N+/foVpvKgeo0cOVJvvfWWnnjiCaujeKTc3FznirP16tVzbpr+//7f/+M1qQGbN2+WdOpenK+//tploSBvb2917dpVY8eOtSqeW1F03CQ4OFhz5szRjBkzXMbnzp2r4OBgi1KZLyEhQU8//bTq1q17xpuAT2PvBPd75513NGnSJPXq1Ut33HGHbr31VjVu3NjqWB7F29tb7du3tzqGx7rxxht199136/nnn1ePHj0kSV999ZXGjh2rm266SZK0ceNGXXLJJRamNN+JEyc0e/ZsffbZZ+rSpUuFezb5fuBeF110kX766Se1atVK7dq104oVK9S9e3d99dVX8vHxsTqe8U6vrhkbG6sXX3zRoxbEYuqamyxbtkxDhw5V+/bt1atXL0mnvplt27ZN7733Hu+uukm/fv30wQcfqH79+urXr98Zz7PZbNybUEO+/fZbvfnmm1q8eLH27duna6+9VsOHD9dNN93kcvkc7vH8889r586devnll895sRRUn6NHj2rMmDFatGiRSktLJUm1atXSiBEj9MILL6hu3brKysqSJIWFhVkX1HB8P7DW+PHjFRAQoL///e9asmSJ7rjjDoWEhCgnJ0djxozRtGnTrI7okQoLC/X555+rY8eO6tixo9Vx3IKi40b79u3TP/7xD/3www+SpEsvvVT33XcfV3TgsdatW6e33npL77zzjk6cOFGlJdlxfoYMGaKVK1eqYcOGuuyyyyq8k/3+++9blMyzHD161LkAStu2bV02TgQ8TXp6utLT03XxxRfr+uuvtzqOx7j11lvVp08fxcfH6/jx4+ratat2794th8OhxYsXa+jQoVZHrHZMXXOjiy66SFOnTrU6BnDBqFu3rurUqSNvb2/9+uuvVsfxCPXr19eQIUOsjuHx6tWrpy5dulgdAzr1JqR06ns0rBEZGelckRY1Z/Xq1Xr88cclnVpS3eFw6MiRI1q4cKGmTJliZNHhio4bHTlyRPPmzdP3338vSbrssst09913KzAw0OJk5rr55pvP+Vzeya4Zu3bt0ltvvaW33npLW7duVd++fTVs2DDdcsst/F0AUCPKy8s1ZcoUPf/88zp69Kgkyd/fX4888ogef/xx2e12ixOa5+OPPz7nc2+44QY3JsFpderU0Y8//qjg4GDFxMSoRYsWmjZtmnJychQaGur8u2ESrui4yaZNmzRw4EDVqVNHPXv2lHTqZsdnnnnGeRMeqh8/OF9YLr/8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2igrK6vCogTLly/XpZdealEqz/LKK69ozpw5uv32251jN9xwg7p06aJRo0ZRdOAR9u/fX+nGxeXl5Tp58qQFiYCa16xZM23fvl0hISEu42vXrmVJ9RrQt29fqyPAQzF1zU0SEhL04IMPasmSJXI4HNq4caOeeeYZTZgwQY899pjV8TzCyZMnFRERUWE8PDxcpaWlFiQCal5oaKjWrFlTYfzdd9+t9KozYKK4uDiNHj1aGzZskM1m04EDB/Tmm29q7Nixuv/++62O5xHWrFmjO+64Q1dccYX2798v6dSiTexpB3fiio6bjBw5UnXq1NHEiRN17NgxDRs2TC1atNCLL76o2267zep4HuHOO+/UK6+8ouTkZJfx2bNna/jw4RalAmpWYmKiRowYof3796u8vFzvv/++tm7dqkWLFumTTz6xOh5QI8aPH6/y8nL1799fx44dU58+feTj46OxY8dq1KhRVscz3nvvvac777xTw4cPr3QxiGXLllmcEKayORwOh9UhTHfs2DEdPXrUuXnl761bt04RERHcoF1NEhISnL8uLS3VggUL1KpVK11++eWSpA0bNignJ0cxMTF66aWXrIoJ1Kg1a9Zo8uTJys7O1tGjR9W9e3clJiZqwIABVkcD3K6srEzr1q1Tly5d5Ofnp+3bt+vo0aMKDQ1VvXr1rI7nEbp166YxY8YoJiZG/v7+ys7OVtu2bbV582b95S9/UW5urtURYSiKjsUCAgKUlZXFHOFq0q9fv3M6z2az6fPPP3dzGgDAhcDX11fff/+92rRpY3UUj+Tn56fvvvtOISEhLkVn586dCg0N1YkTJ6yOCEMxdc1i9MzqxcouAID/1qlTJ+3cuZOiYxEWg4BVKDoAYJgGDRrIZrOd07mHDx92cxrAelOmTNHYsWP19NNPKzw8vMLmuAEBARYl8wynF4OYP3++czGI9PR0jR07Vk888YTV8WAwig4AGCYlJcXqCMAF5brrrpN0aouB378J4HA4ZLPZVFZWZlU0j8BiELAK9+hY7PdzVQEAQPX74osvznqcfV5qRklJCYtBoEZRdCzGYgQA3K2srEwffPCBvv/+e0mn9ta58cYbVasWF/UBuF9BQYHKysrUsGFDl/HDhw+rVq1aTB2E2/BdzmL0TADu9O233+qGG25Qbm6uOnToIEmaPn26mjRpon/961/q1KmTxQkB99iyZYs6deoku92uLVu2nPXcLl261FAqz3Tbbbfp+uuv1wMPPOAy/vbbb+vjjz9mHx24DVd03Ki0tFSrVq3Sjh07NGzYMPn7++vAgQMKCAjgci2AGhEZGakmTZpo4cKFatCggSTpl19+0V133aX8/HytX7/e4oSAe9jtduXm5qpp06ay2+2y2WyVvrnIPTru17BhQ61bt06XXnqpy/gPP/yg3r176+eff7YoGUzHFR032bNnjwYNGqScnBwVFxfr2muvlb+/v6ZPn67i4mKlpqZaHRGAB8jKytKmTZucJUc6tSrbM888ox49eliYDHCvXbt2qUmTJs5fwzrFxcUqLS2tMH7y5EkdP37cgkTwFHarA5hq9OjRioiI0C+//KI6deo4x4cMGaK0tDQLkwHwJJdccony8vIqjB88eFDt27e3IBFQM1q3bu1cYa1169ZnfcC9evbsqdmzZ1cYT01NVXh4uAWJ4Cm4ouMma9as0fr16+Xt7e0yHhISov3791uUCoCnSUpK0kMPPaQnn3xSl19+uSTpyy+/1OTJkzV9+nQVFhY6z+WGYJjk448/Pudzb7jhBjcmwZQpUxQVFaXs7Gz1799fkpSWlqavvvpKK1assDgdTMY9Om7SoEEDrVu3TqGhoS5LSK9du1ZDhw6t9B1WAKhudvtvF+5Pv7t9+p/933/MfQowze//35dU4R6d3++nw//77pedna0ZM2YoKytLderUUZcuXTRhwgRdfPHFVkeDwbii4yYDBgxQSkqK81KtzWbT0aNHNWnSJOfGZQDgbitXrrQ6AmCJ8vJy568/++wzjRs3TlOnTlVkZKQkKT09XRMnTtTUqVOtiugRTp48qb/97W964okn9Oabb1odBx6GKzpusm/fPg0cOFAOh0Pbtm1TRESEtm3bpsaNG2v16tVq2rSp1REBAPAInTp1Umpqqq688kqX8TVr1ujee+917jEF9wgMDFRWVpbatGljdRR4GIqOG5WWlmrJkiXKzs7W0aNH1b17dw0fPtxlcQIAcLcTJ05oy5YtOnjwoMu73BL3JsAz1KlTR1999VWFfaO2bNmiXr16sfKXm40YMUJhYWEaM2aM1VHgYSg6AGCw5cuXKyYmRocOHapwjPty4Cn69OkjX19fvf766woKCpIk5eXlKSYmRidOnNAXX3xhcUKzTZkyRc8//7z69++v8PBw1a1b1+X4Qw89ZFEymI6i4yZJSUkKCgrS3Xff7TI+f/585efna9y4cRYlA+BJLr74Yg0YMECJiYnOH/AAT7N9+3YNGTJEP/74o4KDgyVJe/fu1cUXX6wPP/yQpdbd7GxT1mw2m3bu3FmDaeBJKDpuEhISorfeektXXHGFy/iGDRt02223sXkZgBoREBCgzZs3q127dlZHASzlcDj06aef6ocffpAkXXrppYqKinJZfQ2AWVh1zU1yc3PVvHnzCuNNmjTRTz/9ZEEiAJ7olltu0apVqyg68Hg2m00DBgzQgAEDrI7isUpKSrRr1y61a9dOtWrxIyjcj//L3CQ4OFjr1q2rcLl23bp1atGihUWpAHial19+WX/961+1Zs0ade7cWbVr13Y5ztx4mGrmzJm699575evrq5kzZ571XP4euNexY8c0atQoLVy4UJL0448/qm3btho1apRatmyp8ePHW5wQpmLqmpvMmDFDM2bM0LPPPqtrrrlG0qldgB977DE98sgjmjBhgsUJAXiCefPm6b777pOvr68aNWrkMk2HufEwWZs2bbRp0yY1atSIe0QsNnr0aK1bt04pKSkaNGiQtmzZorZt2+qjjz7Sk08+qc2bN1sdEYai6LiJw+HQ+PHjNXPmTJWUlEiSfH19NW7cOCUmJlqcDoCnaNasmR566CGNHz++wk7xgCc6/WMP9+bUnNatW2vJkiW6/PLL5e/vr+zsbLVt21bbt29X9+7dVVhYaHVEGIrvem5is9k0ffp05efn68svv1R2drYOHz5MyQFQo0pKShQdHU3JgcebN2+eOnXqJF9fX/n6+qpTp06aO3eu1bE8Qn5+fqUbpRcVFVE44VZ853OzevXqqUePHurUqZN8fHysjgPAw4wYMUJLliyxOgZgqcTERI0ePVrXX3+93nnnHb3zzju6/vrrNWbMGN6ArAERERFaunSp8+PT5Wbu3LmKjIy0KhY8AFPX3KSoqEjTpk1TWlpapbuRMx8YQE146KGHtGjRInXt2lVdunSpsBhBcnKyRcmAmtOkSRPNnDlTt99+u8v4P//5T40aNarSDXVRfdauXau//OUvuuOOO7RgwQL97W9/03fffaf169friy++UHh4uNURYShWXXOTkSNH6osvvtCdd96p5s2bc2kWgCW+/vprdevWTZL0zTffuBzj3yV4ipMnTyoiIqLCeHh4uEpLSy1I5FmuvPJKZWVladq0aercubNWrFih7t27Kz09XZ07d7Y6HgzGFR03qV+/vpYuXarevXtbHQUAAI82atQo1a5du8IVzLFjx+r48eOaNWuWRckAuBNXdNykQYMGatiwodUxAADwSAkJCc5f22w2zZ07VytWrNDll18uSdqwYYNycnIUExNjVUSPUlZWpg8++EDff/+9JCk0NFQ33ngjG4fCrbii4yZvvPGGPvroIy1cuFB+fn5WxwHgQW6++WYtWLBAAQEBuvnmm8967vvvv19DqYCa1a9fv3M6z2az6fPPP3dzGs/27bff6oYbblBubq46dOgg6dSmoU2aNNG//vUvderUyeKEMBU12k2ef/557dixQ0FBQQoJCalwA3BmZqZFyQCYLjAw0Hn/TWBgoMVpAGusXLnS6gj4PyNHjtRll12mTZs2qUGDBpKkX375RXfddZfuvfderV+/3uKEMBVXdNzkqaeeOuvxSZMm1VASAJ7s+PHjKi8vV926dSVJu3fv1ocffqhLL71UAwcOtDgdAE9Qp04dbdq0SZdddpnL+DfffKMePXro+PHjFiWD6bii4yYUGQAXghtvvFE333yz7rvvPh05ckSXX365ateurUOHDik5OVn333+/1REBGO6SSy5RXl5ehaJz8OBBtW/f3qJU8ARsGOpGR44c0dy5czVhwgQdPnxY0qkpa/v377c4GQBPkZmZqauuukqS9O677yooKEh79uzRokWLNHPmTIvTAfAESUlJeuihh/Tuu+9q37592rdvn9599109/PDDmj59ugoLC50PoDoxdc1NtmzZoqioKAUGBmr37t3aunWr2rZtq4kTJyonJ0eLFi2yOiIAD+Dn56cffvhBrVq10q233qrLLrtMkyZN0t69e9WhQwcdO3bM6ogADGe3//a++un7B0//+Pn7j202m8rKymo+IIzF1DU3SUhI0F133aUZM2bI39/fOX7ddddp2LBhFiYD4Enat2+vDz/8UEOGDNF//vMfjRkzRtKpKSMBAQEWpwPgCVgYAlah6LjJV199pVdffbXCeMuWLZWbm2tBIgCeKDExUcOGDdOYMWPUv39/RUZGSpJWrFihbt26WZwOgCfo27fvOZ33wAMP6LLLLlPjxo3dnAiegnt03MTHx6fSuaan140HgJpwyy23KCcnR5s2bdLy5cud4/37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\n"
          },
          "metadata": {}
        }
      ],
      "source": [
        "# Sort model results by f1-score\n",
        "all_model_results.sort_values(\"f1\", ascending=False)[\"f1\"].plot(kind=\"bar\", figsize=(10, 7));"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "pv2iE0TPGdNy"
      },
      "source": [
        "Drilling down into a single metric we see our USE TensorFlow Hub models performing  better than all of the other models. Interestingly, the baseline's F1-score isn't too far off the rest of the deeper models.\n",
        "\n",
        "We can also visualize all of our model's training logs using TensorBoard.dev."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 104,
      "metadata": {
        "id": "2Ca8TalwGhPf"
      },
      "outputs": [],
      "source": [
        "# # View tensorboard logs of transfer learning modelling experiments (should be 4 models)\n",
        "# # Upload TensorBoard dev records\n",
        "# !tensorboard dev upload --logdir ./model_logs \\\n",
        "#   --name \"NLP modelling experiments\" \\\n",
        "#   --description \"A series of different NLP modellings experiments with various models\" \\\n",
        "#   --one_shot # exits the uploader when upload has finished"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "uIYVXCUJ3FBn"
      },
      "source": [
        "The TensorBoard logs of the different modelling experiments we ran can be viewed here: https://tensorboard.dev/experiment/LkoAakb7QIKBZ0RL97cXbw/"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 105,
      "metadata": {
        "id": "Os7dv00u21jg"
      },
      "outputs": [],
      "source": [
        "# If you need to remove previous experiments, you can do so using the following command\n",
        "# !tensorboard dev delete --experiment_id EXPERIMENT_ID_TO_DELETE"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "GGVZhTTiGdd5"
      },
      "source": [
        "## Combining our models (model ensembling/stacking)\n",
        "\n",
        "Many production systems use an **ensemble** (multiple different models combined) of models to make a prediction.\n",
        "\n",
        "The idea behind model stacking is that if several uncorrelated models agree on a prediction, then the prediction must be more robust than a prediction made by a singular model.\n",
        "\n",
        "The keyword in the sentence above is **uncorrelated**, which is another way of saying, different types of models. For example, in our case, we might combine our baseline, our bidirectional model and our TensorFlow Hub USE model.\n",
        "\n",
        "Although these models are all trained on the same data, they all have a different way of finding patterns.\n",
        "\n",
        "If we were to use three similarly trained models, such as three LSTM models, the predictions they output will likely be very similar.\n",
        "\n",
        "Think of it as trying to decide where to eat with your friends. If you all have similar tastes, you'll probably all pick the same restaurant. But if you've all got different tastes and still end up picking the same restaurant, the restaurant must be good.\n",
        "\n",
        "Since we're working with a classification problem, there are a few of ways we can combine our models:\n",
        "1. **Averaging** - Take the output prediction probabilities of each model for each sample, combine them and then average them.\n",
        "2. **Majority vote (mode)** - Make class predictions with each of your models on all samples, the predicted class is the one in majority. For example, if three different models predict `[1, 0, 1]` respectively, the majority class is `1`, therefore, that would be the predicted label.\n",
        "3. **Model stacking** - Take the outputs of each of your chosen models and use them as inputs to another model.\n",
        "\n",
        "> 📖 **Resource:** The above methods for model stacking/ensembling were adapted from Chapter 6 of the [Machine Learning Engineering Book](http://www.mlebook.com/wiki/doku.php) by Andriy Burkov. If you're looking to enter the field of machine learning engineering, not only building models but production-scale machine learning systems, I'd highly recommend reading it in its entirety.\n",
        "\n",
        "Again, the concept of model stacking is best seen in action.\n",
        "\n",
        "We're going to combine our baseline model (`model_0`), LSTM model (`model_2`) and our USE model trained on the full training data (`model_6`) by averaging the combined prediction probabilities of each."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 106,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "t63u8PCCm-yo",
        "outputId": "37533bb0-0fc4-4ade-e513-27ed85c4d5cb"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "<tf.Tensor: shape=(20,), dtype=float32, numpy=\n",
              "array([0., 1., 1., 0., 0., 1., 1., 1., 1., 0., 0., 1., 0., 1., 0., 0., 0.,\n",
              "       0., 0., 1.], dtype=float32)>"
            ]
          },
          "metadata": {},
          "execution_count": 106
        }
      ],
      "source": [
        "# Get mean pred probs for 3 models\n",
        "baseline_pred_probs = np.max(model_0.predict_proba(val_sentences), axis=1) # get the prediction probabilities from baseline model\n",
        "combined_pred_probs = baseline_pred_probs + tf.squeeze(model_2_pred_probs, axis=1) + tf.squeeze(model_6_pred_probs)\n",
        "combined_preds = tf.round(combined_pred_probs/3) # average and round the prediction probabilities to get prediction classes\n",
        "combined_preds[:20]"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "6abZa7wqlXSI"
      },
      "source": [
        "Wonderful! We've got a combined predictions array of different classes, let's evaluate them against the true labels and add our stacked model's results to our `all_model_results` DataFrame."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 107,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "ieYvhDiev8Et",
        "outputId": "a9779e7d-85d7-4437-954a-745051dfe980"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "{'accuracy': 77.95275590551181,\n",
              " 'precision': 0.7792442137914578,\n",
              " 'recall': 0.7795275590551181,\n",
              " 'f1': 0.7789463852322546}"
            ]
          },
          "metadata": {},
          "execution_count": 107
        }
      ],
      "source": [
        "# Calculate results from averaging the prediction probabilities\n",
        "ensemble_results = calculate_results(val_labels, combined_preds)\n",
        "ensemble_results"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 108,
      "metadata": {
        "id": "132EHlUUpRrP"
      },
      "outputs": [],
      "source": [
        "# Add our combined model's results to the results DataFrame\n",
        "all_model_results.loc[\"ensemble_results\"] = ensemble_results"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 109,
      "metadata": {
        "id": "Pm2P1zsvpZ3D"
      },
      "outputs": [],
      "source": [
        "# Convert the accuracy to the same scale as the rest of the results\n",
        "all_model_results.loc[\"ensemble_results\"][\"accuracy\"] = all_model_results.loc[\"ensemble_results\"][\"accuracy\"]/100"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 110,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 331
        },
        "id": "trmdZ6eEpwHI",
        "outputId": "760e8666-f50e-4183-d8d1-de20867b4c25"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "                         accuracy  precision    recall        f1\n",
              "baseline                 0.792651   0.811139  0.792651  0.786219\n",
              "simple_dense             0.786089   0.790328  0.786089  0.783297\n",
              "lstm                     0.755906   0.756716  0.755906  0.753960\n",
              "gru                      0.775591   0.776327  0.775591  0.774090\n",
              "bidirectional            0.767717   0.767545  0.767717  0.766793\n",
              "conv1d                   0.787402   0.790061  0.787402  0.785228\n",
              "tf_hub_sentence_encoder  0.817585   0.820602  0.817585  0.815879\n",
              "tf_hub_10_percent_data   0.770341   0.776012  0.770341  0.766538\n",
              "ensemble_results         0.779528   0.779244  0.779528  0.778946"
            ],
            "text/html": [
              "\n",
              "  <div id=\"df-732a719c-3154-4dc3-9244-2e3d783e2015\">\n",
              "    <div class=\"colab-df-container\">\n",
              "      <div>\n",
              "<style scoped>\n",
              "    .dataframe tbody tr th:only-of-type {\n",
              "        vertical-align: middle;\n",
              "    }\n",
              "\n",
              "    .dataframe tbody tr th {\n",
              "        vertical-align: top;\n",
              "    }\n",
              "\n",
              "    .dataframe thead th {\n",
              "        text-align: right;\n",
              "    }\n",
              "</style>\n",
              "<table border=\"1\" class=\"dataframe\">\n",
              "  <thead>\n",
              "    <tr style=\"text-align: right;\">\n",
              "      <th></th>\n",
              "      <th>accuracy</th>\n",
              "      <th>precision</th>\n",
              "      <th>recall</th>\n",
              "      <th>f1</th>\n",
              "    </tr>\n",
              "  </thead>\n",
              "  <tbody>\n",
              "    <tr>\n",
              "      <th>baseline</th>\n",
              "      <td>0.792651</td>\n",
              "      <td>0.811139</td>\n",
              "      <td>0.792651</td>\n",
              "      <td>0.786219</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>simple_dense</th>\n",
              "      <td>0.786089</td>\n",
              "      <td>0.790328</td>\n",
              "      <td>0.786089</td>\n",
              "      <td>0.783297</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>lstm</th>\n",
              "      <td>0.755906</td>\n",
              "      <td>0.756716</td>\n",
              "      <td>0.755906</td>\n",
              "      <td>0.753960</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>gru</th>\n",
              "      <td>0.775591</td>\n",
              "      <td>0.776327</td>\n",
              "      <td>0.775591</td>\n",
              "      <td>0.774090</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>bidirectional</th>\n",
              "      <td>0.767717</td>\n",
              "      <td>0.767545</td>\n",
              "      <td>0.767717</td>\n",
              "      <td>0.766793</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>conv1d</th>\n",
              "      <td>0.787402</td>\n",
              "      <td>0.790061</td>\n",
              "      <td>0.787402</td>\n",
              "      <td>0.785228</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>tf_hub_sentence_encoder</th>\n",
              "      <td>0.817585</td>\n",
              "      <td>0.820602</td>\n",
              "      <td>0.817585</td>\n",
              "      <td>0.815879</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>tf_hub_10_percent_data</th>\n",
              "      <td>0.770341</td>\n",
              "      <td>0.776012</td>\n",
              "      <td>0.770341</td>\n",
              "      <td>0.766538</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>ensemble_results</th>\n",
              "      <td>0.779528</td>\n",
              "      <td>0.779244</td>\n",
              "      <td>0.779528</td>\n",
              "      <td>0.778946</td>\n",
              "    </tr>\n",
              "  </tbody>\n",
              "</table>\n",
              "</div>\n",
              "      <button class=\"colab-df-convert\" onclick=\"convertToInteractive('df-732a719c-3154-4dc3-9244-2e3d783e2015')\"\n",
              "              title=\"Convert this dataframe to an interactive table.\"\n",
              "              style=\"display:none;\">\n",
              "        \n",
              "  <svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\"viewBox=\"0 0 24 24\"\n",
              "       width=\"24px\">\n",
              "    <path d=\"M0 0h24v24H0V0z\" fill=\"none\"/>\n",
              "    <path d=\"M18.56 5.44l.94 2.06.94-2.06 2.06-.94-2.06-.94-.94-2.06-.94 2.06-2.06.94zm-11 1L8.5 8.5l.94-2.06 2.06-.94-2.06-.94L8.5 2.5l-.94 2.06-2.06.94zm10 10l.94 2.06.94-2.06 2.06-.94-2.06-.94-.94-2.06-.94 2.06-2.06.94z\"/><path d=\"M17.41 7.96l-1.37-1.37c-.4-.4-.92-.59-1.43-.59-.52 0-1.04.2-1.43.59L10.3 9.45l-7.72 7.72c-.78.78-.78 2.05 0 2.83L4 21.41c.39.39.9.59 1.41.59.51 0 1.02-.2 1.41-.59l7.78-7.78 2.81-2.81c.8-.78.8-2.07 0-2.86zM5.41 20L4 18.59l7.72-7.72 1.47 1.35L5.41 20z\"/>\n",
              "  </svg>\n",
              "      </button>\n",
              "      \n",
              "  <style>\n",
              "    .colab-df-container {\n",
              "      display:flex;\n",
              "      flex-wrap:wrap;\n",
              "      gap: 12px;\n",
              "    }\n",
              "\n",
              "    .colab-df-convert {\n",
              "      background-color: #E8F0FE;\n",
              "      border: none;\n",
              "      border-radius: 50%;\n",
              "      cursor: pointer;\n",
              "      display: none;\n",
              "      fill: #1967D2;\n",
              "      height: 32px;\n",
              "      padding: 0 0 0 0;\n",
              "      width: 32px;\n",
              "    }\n",
              "\n",
              "    .colab-df-convert:hover {\n",
              "      background-color: #E2EBFA;\n",
              "      box-shadow: 0px 1px 2px rgba(60, 64, 67, 0.3), 0px 1px 3px 1px rgba(60, 64, 67, 0.15);\n",
              "      fill: #174EA6;\n",
              "    }\n",
              "\n",
              "    [theme=dark] .colab-df-convert {\n",
              "      background-color: #3B4455;\n",
              "      fill: #D2E3FC;\n",
              "    }\n",
              "\n",
              "    [theme=dark] .colab-df-convert:hover {\n",
              "      background-color: #434B5C;\n",
              "      box-shadow: 0px 1px 3px 1px rgba(0, 0, 0, 0.15);\n",
              "      filter: drop-shadow(0px 1px 2px rgba(0, 0, 0, 0.3));\n",
              "      fill: #FFFFFF;\n",
              "    }\n",
              "  </style>\n",
              "\n",
              "      <script>\n",
              "        const buttonEl =\n",
              "          document.querySelector('#df-732a719c-3154-4dc3-9244-2e3d783e2015 button.colab-df-convert');\n",
              "        buttonEl.style.display =\n",
              "          google.colab.kernel.accessAllowed ? 'block' : 'none';\n",
              "\n",
              "        async function convertToInteractive(key) {\n",
              "          const element = document.querySelector('#df-732a719c-3154-4dc3-9244-2e3d783e2015');\n",
              "          const dataTable =\n",
              "            await google.colab.kernel.invokeFunction('convertToInteractive',\n",
              "                                                     [key], {});\n",
              "          if (!dataTable) return;\n",
              "\n",
              "          const docLinkHtml = 'Like what you see? Visit the ' +\n",
              "            '<a target=\"_blank\" href=https://colab.research.google.com/notebooks/data_table.ipynb>data table notebook</a>'\n",
              "            + ' to learn more about interactive tables.';\n",
              "          element.innerHTML = '';\n",
              "          dataTable['output_type'] = 'display_data';\n",
              "          await google.colab.output.renderOutput(dataTable, element);\n",
              "          const docLink = document.createElement('div');\n",
              "          docLink.innerHTML = docLinkHtml;\n",
              "          element.appendChild(docLink);\n",
              "        }\n",
              "      </script>\n",
              "    </div>\n",
              "  </div>\n",
              "  "
            ]
          },
          "metadata": {},
          "execution_count": 110
        }
      ],
      "source": [
        "all_model_results"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "HZwqwF_swdIA"
      },
      "source": [
        "How did the stacked model go against the other models?\n",
        "\n",
        "> 🔑 **Note:** It seems many of our model's results are similar. This may mean there are some limitations to what can be learned from our data. When many of your modelling experiments return similar results, it's a good idea to revisit your data, we'll do this shortly."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "UpwErZOgX_nC"
      },
      "source": [
        "## Saving and loading a trained model\n",
        "\n",
        "Although training time didn't take very long, it's good practice to save your trained models to avoid having to retrain them.\n",
        "\n",
        "Saving your models also enables you to export them for use elsewhere outside of your notebooks, such as in a web application.\n",
        "\n",
        "There are two main ways of [saving a model in TensorFlow](https://www.tensorflow.org/tutorials/keras/save_and_load#save_the_entire_model):\n",
        "1. The `HDF5` format. \n",
        "2. The `SavedModel` format (default).\n",
        "\n",
        "Let's take a look at both."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 111,
      "metadata": {
        "id": "SlwjGFVyX-_T"
      },
      "outputs": [],
      "source": [
        "# Save TF Hub Sentence Encoder model to HDF5 format\n",
        "model_6.save(\"model_6.h5\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "Cp6zvmprm9A3"
      },
      "source": [
        "If you save a model as a `HDF5`, when loading it back in, you need to let [TensorFlow know about any custom objects you've used](https://www.tensorflow.org/tutorials/keras/save_and_load#saving_custom_objects) (e.g. components which aren't built from pure TensorFlow, such as TensorFlow Hub components)."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 112,
      "metadata": {
        "id": "sSINZ0Q-nRb2"
      },
      "outputs": [],
      "source": [
        "# Load model with custom Hub Layer (required with HDF5 format)\n",
        "loaded_model_6 = tf.keras.models.load_model(\"model_6.h5\", \n",
        "                                            custom_objects={\"KerasLayer\": hub.KerasLayer})"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 113,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "G4BCJ8iXnZ4r",
        "outputId": "dd5e28e0-2c85-453d-be7e-ffd5c3caec07"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "24/24 [==============================] - 1s 7ms/step - loss: 0.4299 - accuracy: 0.8176\n"
          ]
        },
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "[0.4298648238182068, 0.817585289478302]"
            ]
          },
          "metadata": {},
          "execution_count": 113
        }
      ],
      "source": [
        "# How does our loaded model perform?\n",
        "loaded_model_6.evaluate(val_sentences, val_labels)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "02rbT4fwn0It"
      },
      "source": [
        "Calling the `save()` method on our target model and passing it a filepath allows us to save our model in the `SavedModel` format. "
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 114,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "e3eVaNBDoMsv",
        "outputId": "27b38b04-dafc-49f4-e690-c3d7aca05ec5"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stderr",
          "text": [
            "WARNING:absl:Function `_wrapped_model` contains input name(s) USE_input with unsupported characters which will be renamed to use_input in the SavedModel.\n"
          ]
        }
      ],
      "source": [
        "# Save TF Hub Sentence Encoder model to SavedModel format (default)\n",
        "model_6.save(\"model_6_SavedModel_format\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "l-t01S-JoOqK"
      },
      "source": [
        "If you use SavedModel format (default), you can reload your model without specifying custom objects using the [`tensorflow.keras.models.load_model()`](https://www.tensorflow.org/tutorials/keras/save_and_load) function."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 115,
      "metadata": {
        "id": "Dw3zf4fVoU5H"
      },
      "outputs": [],
      "source": [
        "# Load TF Hub Sentence Encoder SavedModel\n",
        "loaded_model_6_SavedModel = tf.keras.models.load_model(\"model_6_SavedModel_format\")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 116,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "IqiPr6iiofi1",
        "outputId": "cc2e3dbc-86fe-4c2d-c895-77d440faf443"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "24/24 [==============================] - 1s 8ms/step - loss: 0.4299 - accuracy: 0.8176\n"
          ]
        },
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "[0.4298648238182068, 0.817585289478302]"
            ]
          },
          "metadata": {},
          "execution_count": 116
        }
      ],
      "source": [
        "# Evaluate loaded SavedModel format\n",
        "loaded_model_6_SavedModel.evaluate(val_sentences, val_labels)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "xzp3SHi3oQ3u"
      },
      "source": [
        "As you can see saving and loading our model with either format results in the same performance.\n",
        "\n",
        "> 🤔 **Question:** Should you used the `SavedModel` format or `HDF5` format?\n",
        "\n",
        "For most use cases, the `SavedModel` format will suffice. However, this is a TensorFlow specific standard. If you need a more general-purpose data standard, `HDF5` might be better. For more, check out the [TensorFlow documentation on saving and loading models](https://www.tensorflow.org/tutorials/keras/save_and_load)."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "V5a1648rG3z1"
      },
      "source": [
        "## Finding the most wrong examples\n",
        "\n",
        "We mentioned before that if many of our modelling experiments are returning similar results, despite using different kinds of models, it's a good idea to return to the data and inspect why this might be.\n",
        "\n",
        "One of the best ways to inspect your data is to sort your model's predictions and find the samples it got *most* wrong, meaning, what predictions had a high prediction probability but turned out to be wrong.\n",
        "\n",
        "Once again, visualization is your friend. Visualize, visualize, visualize.\n",
        "\n",
        "To make things visual, let's take our best performing model's prediction probabilities and classes along with the validation samples (text and ground truth labels) and combine them in a pandas DataFrame.\n",
        "\n",
        "* If our best model still isn't perfect, what examples is it getting wrong? \n",
        "* Which ones are the *most* wrong?\n",
        "* Are there some labels which are wrong? E.g. the model gets it right but the ground truth label doesn't reflect this"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 117,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 206
        },
        "id": "gnHfX--TwMIW",
        "outputId": "57eaf599-c9e2-4541-8fd4-5d1575cc4975"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "                                                text  target  pred  pred_prob\n",
              "0  DFR EP016 Monthly Meltdown - On Dnbheaven 2015...       0   0.0   0.148141\n",
              "1  FedEx no longer to transport bioterror germs i...       0   1.0   0.740579\n",
              "2  Gunmen kill four in El Salvador bus attack: Su...       1   1.0   0.988647\n",
              "3  @camilacabello97 Internally and externally scr...       1   0.0   0.224560\n",
              "4  Radiation emergency #preparedness starts with ...       1   1.0   0.740494"
            ],
            "text/html": [
              "\n",
              "  <div id=\"df-941fe3a6-fa8d-43af-a7ed-69e058076d7a\">\n",
              "    <div class=\"colab-df-container\">\n",
              "      <div>\n",
              "<style scoped>\n",
              "    .dataframe tbody tr th:only-of-type {\n",
              "        vertical-align: middle;\n",
              "    }\n",
              "\n",
              "    .dataframe tbody tr th {\n",
              "        vertical-align: top;\n",
              "    }\n",
              "\n",
              "    .dataframe thead th {\n",
              "        text-align: right;\n",
              "    }\n",
              "</style>\n",
              "<table border=\"1\" class=\"dataframe\">\n",
              "  <thead>\n",
              "    <tr style=\"text-align: right;\">\n",
              "      <th></th>\n",
              "      <th>text</th>\n",
              "      <th>target</th>\n",
              "      <th>pred</th>\n",
              "      <th>pred_prob</th>\n",
              "    </tr>\n",
              "  </thead>\n",
              "  <tbody>\n",
              "    <tr>\n",
              "      <th>0</th>\n",
              "      <td>DFR EP016 Monthly Meltdown - On Dnbheaven 2015...</td>\n",
              "      <td>0</td>\n",
              "      <td>0.0</td>\n",
              "      <td>0.148141</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>1</th>\n",
              "      <td>FedEx no longer to transport bioterror germs i...</td>\n",
              "      <td>0</td>\n",
              "      <td>1.0</td>\n",
              "      <td>0.740579</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>2</th>\n",
              "      <td>Gunmen kill four in El Salvador bus attack: Su...</td>\n",
              "      <td>1</td>\n",
              "      <td>1.0</td>\n",
              "      <td>0.988647</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>3</th>\n",
              "      <td>@camilacabello97 Internally and externally scr...</td>\n",
              "      <td>1</td>\n",
              "      <td>0.0</td>\n",
              "      <td>0.224560</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>4</th>\n",
              "      <td>Radiation emergency #preparedness starts with ...</td>\n",
              "      <td>1</td>\n",
              "      <td>1.0</td>\n",
              "      <td>0.740494</td>\n",
              "    </tr>\n",
              "  </tbody>\n",
              "</table>\n",
              "</div>\n",
              "      <button class=\"colab-df-convert\" onclick=\"convertToInteractive('df-941fe3a6-fa8d-43af-a7ed-69e058076d7a')\"\n",
              "              title=\"Convert this dataframe to an interactive table.\"\n",
              "              style=\"display:none;\">\n",
              "        \n",
              "  <svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\"viewBox=\"0 0 24 24\"\n",
              "       width=\"24px\">\n",
              "    <path d=\"M0 0h24v24H0V0z\" fill=\"none\"/>\n",
              "    <path d=\"M18.56 5.44l.94 2.06.94-2.06 2.06-.94-2.06-.94-.94-2.06-.94 2.06-2.06.94zm-11 1L8.5 8.5l.94-2.06 2.06-.94-2.06-.94L8.5 2.5l-.94 2.06-2.06.94zm10 10l.94 2.06.94-2.06 2.06-.94-2.06-.94-.94-2.06-.94 2.06-2.06.94z\"/><path d=\"M17.41 7.96l-1.37-1.37c-.4-.4-.92-.59-1.43-.59-.52 0-1.04.2-1.43.59L10.3 9.45l-7.72 7.72c-.78.78-.78 2.05 0 2.83L4 21.41c.39.39.9.59 1.41.59.51 0 1.02-.2 1.41-.59l7.78-7.78 2.81-2.81c.8-.78.8-2.07 0-2.86zM5.41 20L4 18.59l7.72-7.72 1.47 1.35L5.41 20z\"/>\n",
              "  </svg>\n",
              "      </button>\n",
              "      \n",
              "  <style>\n",
              "    .colab-df-container {\n",
              "      display:flex;\n",
              "      flex-wrap:wrap;\n",
              "      gap: 12px;\n",
              "    }\n",
              "\n",
              "    .colab-df-convert {\n",
              "      background-color: #E8F0FE;\n",
              "      border: none;\n",
              "      border-radius: 50%;\n",
              "      cursor: pointer;\n",
              "      display: none;\n",
              "      fill: #1967D2;\n",
              "      height: 32px;\n",
              "      padding: 0 0 0 0;\n",
              "      width: 32px;\n",
              "    }\n",
              "\n",
              "    .colab-df-convert:hover {\n",
              "      background-color: #E2EBFA;\n",
              "      box-shadow: 0px 1px 2px rgba(60, 64, 67, 0.3), 0px 1px 3px 1px rgba(60, 64, 67, 0.15);\n",
              "      fill: #174EA6;\n",
              "    }\n",
              "\n",
              "    [theme=dark] .colab-df-convert {\n",
              "      background-color: #3B4455;\n",
              "      fill: #D2E3FC;\n",
              "    }\n",
              "\n",
              "    [theme=dark] .colab-df-convert:hover {\n",
              "      background-color: #434B5C;\n",
              "      box-shadow: 0px 1px 3px 1px rgba(0, 0, 0, 0.15);\n",
              "      filter: drop-shadow(0px 1px 2px rgba(0, 0, 0, 0.3));\n",
              "      fill: #FFFFFF;\n",
              "    }\n",
              "  </style>\n",
              "\n",
              "      <script>\n",
              "        const buttonEl =\n",
              "          document.querySelector('#df-941fe3a6-fa8d-43af-a7ed-69e058076d7a button.colab-df-convert');\n",
              "        buttonEl.style.display =\n",
              "          google.colab.kernel.accessAllowed ? 'block' : 'none';\n",
              "\n",
              "        async function convertToInteractive(key) {\n",
              "          const element = document.querySelector('#df-941fe3a6-fa8d-43af-a7ed-69e058076d7a');\n",
              "          const dataTable =\n",
              "            await google.colab.kernel.invokeFunction('convertToInteractive',\n",
              "                                                     [key], {});\n",
              "          if (!dataTable) return;\n",
              "\n",
              "          const docLinkHtml = 'Like what you see? Visit the ' +\n",
              "            '<a target=\"_blank\" href=https://colab.research.google.com/notebooks/data_table.ipynb>data table notebook</a>'\n",
              "            + ' to learn more about interactive tables.';\n",
              "          element.innerHTML = '';\n",
              "          dataTable['output_type'] = 'display_data';\n",
              "          await google.colab.output.renderOutput(dataTable, element);\n",
              "          const docLink = document.createElement('div');\n",
              "          docLink.innerHTML = docLinkHtml;\n",
              "          element.appendChild(docLink);\n",
              "        }\n",
              "      </script>\n",
              "    </div>\n",
              "  </div>\n",
              "  "
            ]
          },
          "metadata": {},
          "execution_count": 117
        }
      ],
      "source": [
        "# Create dataframe with validation sentences and best performing model predictions\n",
        "val_df = pd.DataFrame({\"text\": val_sentences,\n",
        "                       \"target\": val_labels,\n",
        "                       \"pred\": model_6_preds,\n",
        "                       \"pred_prob\": tf.squeeze(model_6_pred_probs)})\n",
        "val_df.head()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "SKJ9dTbPrIG4"
      },
      "source": [
        "Oh yeah! Now let's find our model's wrong predictions (where `target != pred`) and sort them by their prediction probability (the `pred_prob` column)."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 118,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 363
        },
        "id": "0DwBXQS1wvZx",
        "outputId": "6baf77fd-b2d8-4c44-c86d-d7490ace1f02"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "                                                  text  target  pred  \\\n",
              "31   ? High Skies - Burning Buildings ? http://t.co...       0   1.0   \n",
              "628  @noah_anyname That's where the concentration c...       0   1.0   \n",
              "759  FedEx will no longer transport bioterror patho...       0   1.0   \n",
              "393  @SonofLiberty357 all illuminated by the bright...       0   1.0   \n",
              "49   @madonnamking RSPCA site multiple 7 story high...       0   1.0   \n",
              "209  Ashes 2015: AustraliaÛªs collapse at Trent Br...       0   1.0   \n",
              "251  @AshGhebranious civil rights continued in the ...       0   1.0   \n",
              "109  [55436] 1950 LIONEL TRAINS SMOKE LOCOMOTIVES W...       0   1.0   \n",
              "698  åÈMGN-AFRICAå¨ pin:263789F4 åÈ Correction: Ten...       0   1.0   \n",
              "695  A look at state actions a year after Ferguson'...       0   1.0   \n",
              "\n",
              "     pred_prob  \n",
              "31    0.906832  \n",
              "628   0.866348  \n",
              "759   0.859502  \n",
              "393   0.855963  \n",
              "49    0.839930  \n",
              "209   0.815515  \n",
              "251   0.807973  \n",
              "109   0.806746  \n",
              "698   0.782425  \n",
              "695   0.759534  "
            ],
            "text/html": [
              "\n",
              "  <div id=\"df-65237c1f-b492-4cba-8952-734fc2d8462d\">\n",
              "    <div class=\"colab-df-container\">\n",
              "      <div>\n",
              "<style scoped>\n",
              "    .dataframe tbody tr th:only-of-type {\n",
              "        vertical-align: middle;\n",
              "    }\n",
              "\n",
              "    .dataframe tbody tr th {\n",
              "        vertical-align: top;\n",
              "    }\n",
              "\n",
              "    .dataframe thead th {\n",
              "        text-align: right;\n",
              "    }\n",
              "</style>\n",
              "<table border=\"1\" class=\"dataframe\">\n",
              "  <thead>\n",
              "    <tr style=\"text-align: right;\">\n",
              "      <th></th>\n",
              "      <th>text</th>\n",
              "      <th>target</th>\n",
              "      <th>pred</th>\n",
              "      <th>pred_prob</th>\n",
              "    </tr>\n",
              "  </thead>\n",
              "  <tbody>\n",
              "    <tr>\n",
              "      <th>31</th>\n",
              "      <td>? High Skies - Burning Buildings ? http://t.co...</td>\n",
              "      <td>0</td>\n",
              "      <td>1.0</td>\n",
              "      <td>0.906832</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>628</th>\n",
              "      <td>@noah_anyname That's where the concentration c...</td>\n",
              "      <td>0</td>\n",
              "      <td>1.0</td>\n",
              "      <td>0.866348</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>759</th>\n",
              "      <td>FedEx will no longer transport bioterror patho...</td>\n",
              "      <td>0</td>\n",
              "      <td>1.0</td>\n",
              "      <td>0.859502</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>393</th>\n",
              "      <td>@SonofLiberty357 all illuminated by the bright...</td>\n",
              "      <td>0</td>\n",
              "      <td>1.0</td>\n",
              "      <td>0.855963</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>49</th>\n",
              "      <td>@madonnamking RSPCA site multiple 7 story high...</td>\n",
              "      <td>0</td>\n",
              "      <td>1.0</td>\n",
              "      <td>0.839930</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>209</th>\n",
              "      <td>Ashes 2015: AustraliaÛªs collapse at Trent Br...</td>\n",
              "      <td>0</td>\n",
              "      <td>1.0</td>\n",
              "      <td>0.815515</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>251</th>\n",
              "      <td>@AshGhebranious civil rights continued in the ...</td>\n",
              "      <td>0</td>\n",
              "      <td>1.0</td>\n",
              "      <td>0.807973</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>109</th>\n",
              "      <td>[55436] 1950 LIONEL TRAINS SMOKE LOCOMOTIVES W...</td>\n",
              "      <td>0</td>\n",
              "      <td>1.0</td>\n",
              "      <td>0.806746</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>698</th>\n",
              "      <td>åÈMGN-AFRICAå¨ pin:263789F4 åÈ Correction: Ten...</td>\n",
              "      <td>0</td>\n",
              "      <td>1.0</td>\n",
              "      <td>0.782425</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>695</th>\n",
              "      <td>A look at state actions a year after Ferguson'...</td>\n",
              "      <td>0</td>\n",
              "      <td>1.0</td>\n",
              "      <td>0.759534</td>\n",
              "    </tr>\n",
              "  </tbody>\n",
              "</table>\n",
              "</div>\n",
              "      <button class=\"colab-df-convert\" onclick=\"convertToInteractive('df-65237c1f-b492-4cba-8952-734fc2d8462d')\"\n",
              "              title=\"Convert this dataframe to an interactive table.\"\n",
              "              style=\"display:none;\">\n",
              "        \n",
              "  <svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\"viewBox=\"0 0 24 24\"\n",
              "       width=\"24px\">\n",
              "    <path d=\"M0 0h24v24H0V0z\" fill=\"none\"/>\n",
              "    <path d=\"M18.56 5.44l.94 2.06.94-2.06 2.06-.94-2.06-.94-.94-2.06-.94 2.06-2.06.94zm-11 1L8.5 8.5l.94-2.06 2.06-.94-2.06-.94L8.5 2.5l-.94 2.06-2.06.94zm10 10l.94 2.06.94-2.06 2.06-.94-2.06-.94-.94-2.06-.94 2.06-2.06.94z\"/><path d=\"M17.41 7.96l-1.37-1.37c-.4-.4-.92-.59-1.43-.59-.52 0-1.04.2-1.43.59L10.3 9.45l-7.72 7.72c-.78.78-.78 2.05 0 2.83L4 21.41c.39.39.9.59 1.41.59.51 0 1.02-.2 1.41-.59l7.78-7.78 2.81-2.81c.8-.78.8-2.07 0-2.86zM5.41 20L4 18.59l7.72-7.72 1.47 1.35L5.41 20z\"/>\n",
              "  </svg>\n",
              "      </button>\n",
              "      \n",
              "  <style>\n",
              "    .colab-df-container {\n",
              "      display:flex;\n",
              "      flex-wrap:wrap;\n",
              "      gap: 12px;\n",
              "    }\n",
              "\n",
              "    .colab-df-convert {\n",
              "      background-color: #E8F0FE;\n",
              "      border: none;\n",
              "      border-radius: 50%;\n",
              "      cursor: pointer;\n",
              "      display: none;\n",
              "      fill: #1967D2;\n",
              "      height: 32px;\n",
              "      padding: 0 0 0 0;\n",
              "      width: 32px;\n",
              "    }\n",
              "\n",
              "    .colab-df-convert:hover {\n",
              "      background-color: #E2EBFA;\n",
              "      box-shadow: 0px 1px 2px rgba(60, 64, 67, 0.3), 0px 1px 3px 1px rgba(60, 64, 67, 0.15);\n",
              "      fill: #174EA6;\n",
              "    }\n",
              "\n",
              "    [theme=dark] .colab-df-convert {\n",
              "      background-color: #3B4455;\n",
              "      fill: #D2E3FC;\n",
              "    }\n",
              "\n",
              "    [theme=dark] .colab-df-convert:hover {\n",
              "      background-color: #434B5C;\n",
              "      box-shadow: 0px 1px 3px 1px rgba(0, 0, 0, 0.15);\n",
              "      filter: drop-shadow(0px 1px 2px rgba(0, 0, 0, 0.3));\n",
              "      fill: #FFFFFF;\n",
              "    }\n",
              "  </style>\n",
              "\n",
              "      <script>\n",
              "        const buttonEl =\n",
              "          document.querySelector('#df-65237c1f-b492-4cba-8952-734fc2d8462d button.colab-df-convert');\n",
              "        buttonEl.style.display =\n",
              "          google.colab.kernel.accessAllowed ? 'block' : 'none';\n",
              "\n",
              "        async function convertToInteractive(key) {\n",
              "          const element = document.querySelector('#df-65237c1f-b492-4cba-8952-734fc2d8462d');\n",
              "          const dataTable =\n",
              "            await google.colab.kernel.invokeFunction('convertToInteractive',\n",
              "                                                     [key], {});\n",
              "          if (!dataTable) return;\n",
              "\n",
              "          const docLinkHtml = 'Like what you see? Visit the ' +\n",
              "            '<a target=\"_blank\" href=https://colab.research.google.com/notebooks/data_table.ipynb>data table notebook</a>'\n",
              "            + ' to learn more about interactive tables.';\n",
              "          element.innerHTML = '';\n",
              "          dataTable['output_type'] = 'display_data';\n",
              "          await google.colab.output.renderOutput(dataTable, element);\n",
              "          const docLink = document.createElement('div');\n",
              "          docLink.innerHTML = docLinkHtml;\n",
              "          element.appendChild(docLink);\n",
              "        }\n",
              "      </script>\n",
              "    </div>\n",
              "  </div>\n",
              "  "
            ]
          },
          "metadata": {},
          "execution_count": 118
        }
      ],
      "source": [
        "# Find the wrong predictions and sort by prediction probabilities\n",
        "most_wrong = val_df[val_df[\"target\"] != val_df[\"pred\"]].sort_values(\"pred_prob\", ascending=False)\n",
        "most_wrong[:10]"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "r3VcRHOusB2D"
      },
      "source": [
        "Finally, we can write some code to visualize the sample text, truth label, prediction class and prediction probability. Because we've sorted our samples by prediction probability, viewing samples from the head of our `most_wrong` DataFrame will show us false positives.\n",
        "\n",
        "A reminder:\n",
        "* `0` = Not a real diaster Tweet\n",
        "* `1` = Real diaster Tweet"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 119,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "xLFYDEsoxRFP",
        "outputId": "75920ca0-eef0-4190-baf2-c21e2164e4f5"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Target: 0, Pred: 1, Prob: 0.9068315625190735\n",
            "Text:\n",
            "? High Skies - Burning Buildings ? http://t.co/uVq41i3Kx2 #nowplaying\n",
            "\n",
            "----\n",
            "\n",
            "Target: 0, Pred: 1, Prob: 0.8663479685783386\n",
            "Text:\n",
            "@noah_anyname That's where the concentration camps and mass murder come in. \n",
            " \n",
            "EVERY. FUCKING. TIME.\n",
            "\n",
            "----\n",
            "\n",
            "Target: 0, Pred: 1, Prob: 0.859502375125885\n",
            "Text:\n",
            "FedEx will no longer transport bioterror pathogens in wake of anthrax lab mishaps http://t.co/lHpgxc4b8J\n",
            "\n",
            "----\n",
            "\n",
            "Target: 0, Pred: 1, Prob: 0.8559632897377014\n",
            "Text:\n",
            "@SonofLiberty357 all illuminated by the brightly burning buildings all around the town!\n",
            "\n",
            "----\n",
            "\n",
            "Target: 0, Pred: 1, Prob: 0.8399295806884766\n",
            "Text:\n",
            "@madonnamking RSPCA site multiple 7 story high rise buildings next to low density character residential in an area that floods\n",
            "\n",
            "----\n",
            "\n",
            "Target: 0, Pred: 1, Prob: 0.8155148029327393\n",
            "Text:\n",
            "Ashes 2015: AustraliaÛªs collapse at Trent Bridge among worst in history: England bundled out Australia for 60 ... http://t.co/t5TrhjUAU0\n",
            "\n",
            "----\n",
            "\n",
            "Target: 0, Pred: 1, Prob: 0.8079732060432434\n",
            "Text:\n",
            "@AshGhebranious civil rights continued in the 60s. And what about trans-generational trauma? if anything we should listen to the Americans.\n",
            "\n",
            "----\n",
            "\n",
            "Target: 0, Pred: 1, Prob: 0.8067457675933838\n",
            "Text:\n",
            "[55436] 1950 LIONEL TRAINS SMOKE LOCOMOTIVES WITH MAGNE-TRACTION INSTRUCTIONS http://t.co/xEZBs3sq0y http://t.co/C2x0QoKGlY\n",
            "\n",
            "----\n",
            "\n",
            "Target: 0, Pred: 1, Prob: 0.7824245095252991\n",
            "Text:\n",
            "åÈMGN-AFRICAå¨ pin:263789F4 åÈ Correction: Tent Collapse Story: Correction: Tent Collapse story åÈ http://t.co/fDJUYvZMrv @wizkidayo\n",
            "\n",
            "----\n",
            "\n",
            "Target: 0, Pred: 1, Prob: 0.7595335841178894\n",
            "Text:\n",
            "A look at state actions a year after Ferguson's upheaval http://t.co/GZEkQWzijq\n",
            "\n",
            "----\n",
            "\n"
          ]
        }
      ],
      "source": [
        "# Check the false positives (model predicted 1 when should've been 0)\n",
        "for row in most_wrong[:10].itertuples(): # loop through the top 10 rows (change the index to view different rows)\n",
        "  _, text, target, pred, prob = row\n",
        "  print(f\"Target: {target}, Pred: {int(pred)}, Prob: {prob}\")\n",
        "  print(f\"Text:\\n{text}\\n\")\n",
        "  print(\"----\\n\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "aXCH9J-UspWg"
      },
      "source": [
        "We can view the bottom end of our `most_wrong` DataFrame to inspect false negatives (model predicts 0, not a real diaster Tweet, when it should've predicted 1, real diaster Tweet)."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 120,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "6EaMchehxwLq",
        "outputId": "4f2effe7-ed64-4f5e-f4e3-b1f0d0464da8"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Target: 1, Pred: 0, Prob: 0.06247330829501152\n",
            "Text:\n",
            "going to redo my nails and watch behind the scenes of desolation of smaug ayyy\n",
            "\n",
            "----\n",
            "\n",
            "Target: 1, Pred: 0, Prob: 0.05949299782514572\n",
            "Text:\n",
            "@BoyInAHorsemask its a panda trapped in a dogs body\n",
            "\n",
            "----\n",
            "\n",
            "Target: 1, Pred: 0, Prob: 0.056083984673023224\n",
            "Text:\n",
            "@willienelson We need help! Horses will die!Please RT &amp; sign petition!Take a stand &amp; be a voice for them! #gilbert23 https://t.co/e8dl1lNCVu\n",
            "\n",
            "----\n",
            "\n",
            "Target: 1, Pred: 0, Prob: 0.055036477744579315\n",
            "Text:\n",
            "Lucas Duda is Ghost Rider. Not the Nic Cage version but an actual 'engulfed in flames' badass. #Mets\n",
            "\n",
            "----\n",
            "\n",
            "Target: 1, Pred: 0, Prob: 0.054454777389764786\n",
            "Text:\n",
            "You can never escape me. Bullets don't harm me. Nothing harms me. But I know pain. I know pain. Sometimes I share it. With someone like you.\n",
            "\n",
            "----\n",
            "\n",
            "Target: 1, Pred: 0, Prob: 0.046157706528902054\n",
            "Text:\n",
            "I get to smoke my shit in peace\n",
            "\n",
            "----\n",
            "\n",
            "Target: 1, Pred: 0, Prob: 0.03960023820400238\n",
            "Text:\n",
            "Why are you deluged with low self-image? Take the quiz: http://t.co/XsPqdOrIqj http://t.co/CQYvFR4UCy\n",
            "\n",
            "----\n",
            "\n",
            "Target: 1, Pred: 0, Prob: 0.03830057382583618\n",
            "Text:\n",
            "Ron &amp; Fez - Dave's High School Crush https://t.co/aN3W16c8F6 via @YouTube\n",
            "\n",
            "----\n",
            "\n",
            "Target: 1, Pred: 0, Prob: 0.03802212327718735\n",
            "Text:\n",
            "@SoonerMagic_ I mean I'm a fan but I don't need a girl sounding off like a damn siren\n",
            "\n",
            "----\n",
            "\n",
            "Target: 1, Pred: 0, Prob: 0.03466600552201271\n",
            "Text:\n",
            "Reddit Will Now QuarantineÛ_ http://t.co/pkUAMXw6pm #onlinecommunities #reddit #amageddon #freespeech #Business http://t.co/PAWvNJ4sAP\n",
            "\n",
            "----\n",
            "\n"
          ]
        }
      ],
      "source": [
        "# Check the most wrong false negatives (model predicted 0 when should've predict 1)\n",
        "for row in most_wrong[-10:].itertuples():\n",
        "  _, text, target, pred, prob = row\n",
        "  print(f\"Target: {target}, Pred: {int(pred)}, Prob: {prob}\")\n",
        "  print(f\"Text:\\n{text}\\n\")\n",
        "  print(\"----\\n\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "lRKQPEAgtpJq"
      },
      "source": [
        "Do you notice anything interesting about the most wrong samples?\n",
        "\n",
        "Are the ground truth labels correct? What do you think would happen if we went back and corrected the labels which aren't?"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "U0W3DWgWJCWs"
      },
      "source": [
        "## Making predictions on the test dataset\n",
        "\n",
        "Alright we've seen how our model's perform on the validation set.\n",
        "\n",
        "But how about the test dataset?\n",
        "\n",
        "We don't have labels for the test dataset so we're going to have to make some predictions and inspect them for ourselves.\n",
        "\n",
        "Let's write some code to make predictions on random samples from the test dataset and visualize them."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 121,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "6Q9lgqoDyequ",
        "outputId": "842934bd-3081-40e2-beff-7e8534e61de2"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "1/1 [==============================] - 0s 79ms/step\n",
            "Pred: 0, Prob: 0.05416637659072876\n",
            "Text:\n",
            "WHAT a day's cricket that was. Has destroyed any plans I had for exercise today.\n",
            "\n",
            "----\n",
            "\n",
            "1/1 [==============================] - 0s 39ms/step\n",
            "Pred: 1, Prob: 0.5330829620361328\n",
            "Text:\n",
            "Any other generation this would've been fatality  http://t.co/zcCtZM9f0o\n",
            "\n",
            "----\n",
            "\n",
            "1/1 [==============================] - 0s 40ms/step\n",
            "Pred: 1, Prob: 0.9940084218978882\n",
            "Text:\n",
            "Arson suspect linked to 30 fires caught in Northern California http://t.co/HkFPyNb4PS\n",
            "\n",
            "----\n",
            "\n",
            "1/1 [==============================] - 0s 40ms/step\n",
            "Pred: 1, Prob: 0.9726524353027344\n",
            "Text:\n",
            "Help support the victims of the Japanese Earthquake and Pacific Tsunami http://t.co/O5GbPBQH http://t.co/MN5wnxf0 #hope4japan #pray4japan\n",
            "\n",
            "----\n",
            "\n",
            "1/1 [==============================] - 0s 40ms/step\n",
            "Pred: 0, Prob: 0.36651405692100525\n",
            "Text:\n",
            "this is from my show last night and im still panicking over the fact i saw sweaty ashton with my own two eyes http://t.co/yyJ76WBC9y\n",
            "\n",
            "----\n",
            "\n",
            "1/1 [==============================] - 0s 41ms/step\n",
            "Pred: 0, Prob: 0.42949625849723816\n",
            "Text:\n",
            "He came to a land which was engulfed in tribal war and turned it into a land of peace i.e. Madinah. #ProphetMuhammad #islam\n",
            "\n",
            "----\n",
            "\n",
            "1/1 [==============================] - 0s 41ms/step\n",
            "Pred: 1, Prob: 0.7450974583625793\n",
            "Text:\n",
            "Jane Kelsey on the FIRE Economy\n",
            "5th Aug 5:30ÛÒ7:30pm\n",
            "Old Govt Buildings Wgton\n",
            "The context &amp; the driver for #TPP and #TRADEinSERVICESAgreement\n",
            "\n",
            "----\n",
            "\n",
            "1/1 [==============================] - 0s 41ms/step\n",
            "Pred: 0, Prob: 0.13024944067001343\n",
            "Text:\n",
            "Detonation fashionable mountaineering electronic watch water-resistant couples leisure tabÛ_ http://t.co/GH48B54riS http://t.co/2PqTm06Lid\n",
            "\n",
            "----\n",
            "\n",
            "1/1 [==============================] - 0s 41ms/step\n",
            "Pred: 1, Prob: 0.8552481532096863\n",
            "Text:\n",
            "@AlbertBrooks Don't like the Ayatollah Khomeini Memorial Nuclear Reactor for the Annihilation of Israel? Racist!\n",
            "\n",
            "----\n",
            "\n",
            "1/1 [==============================] - 0s 40ms/step\n",
            "Pred: 0, Prob: 0.06269928067922592\n",
            "Text:\n",
            "Can you imagine how traumatised Makoto would be if he could see himself in the dub (aka Jersey Shore AU) rn? Well done America\n",
            "\n",
            "----\n",
            "\n"
          ]
        }
      ],
      "source": [
        "# Making predictions on the test dataset\n",
        "test_sentences = test_df[\"text\"].to_list()\n",
        "test_samples = random.sample(test_sentences, 10)\n",
        "for test_sample in test_samples:\n",
        "  pred_prob = tf.squeeze(model_6.predict([test_sample])) # has to be list\n",
        "  pred = tf.round(pred_prob)\n",
        "  print(f\"Pred: {int(pred)}, Prob: {pred_prob}\")\n",
        "  print(f\"Text:\\n{test_sample}\\n\")\n",
        "  print(\"----\\n\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "QcvI5zgJ0Tgp"
      },
      "source": [
        "How do our model's predictions look on the test dataset?\n",
        "\n",
        "It's important to do these kind of visualization checks as often as possible to get a glance of how your model performs on unseen data and subsequently how it might perform on the real test: Tweets from the wild."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "eT1jhk8xdod5"
      },
      "source": [
        "## Predicting on Tweets from the wild\n",
        "\n",
        "How about we find some Tweets and use our model to predict whether or not they're about a diaster or not?\n",
        "\n",
        "To start, let's take one of my own [Tweets on living life like an ensemble model](https://twitter.com/mrdbourke/status/1313649328351662082). "
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 122,
      "metadata": {
        "id": "qHmXxuPH0aUB"
      },
      "outputs": [],
      "source": [
        "# Turn Tweet into string\n",
        "daniels_tweet = \"Life like an ensemble: take the best choices from others and make your own\""
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "uPbZaGznvbEx"
      },
      "source": [
        "Now we'll write a small function to take a model and an example sentence and return a prediction."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 123,
      "metadata": {
        "id": "KyH9tn9upjld"
      },
      "outputs": [],
      "source": [
        "def predict_on_sentence(model, sentence):\n",
        "  \"\"\"\n",
        "  Uses model to make a prediction on sentence.\n",
        "\n",
        "  Returns the sentence, the predicted label and the prediction probability.\n",
        "  \"\"\"\n",
        "  pred_prob = model.predict([sentence])\n",
        "  pred_label = tf.squeeze(tf.round(pred_prob)).numpy()\n",
        "  print(f\"Pred: {pred_label}\", \"(real disaster)\" if pred_label > 0 else \"(not real disaster)\", f\"Prob: {pred_prob[0][0]}\")\n",
        "  print(f\"Text:\\n{sentence}\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "IvCG4RuUvj6d"
      },
      "source": [
        "Great! Time to test our model out."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 124,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "BxONpJV8qmWP",
        "outputId": "9ce8802e-a787-4b09-88b1-84e7e12489a4"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "1/1 [==============================] - 0s 39ms/step\n",
            "Pred: 0.0 (not real disaster) Prob: 0.044768452644348145\n",
            "Text:\n",
            "Life like an ensemble: take the best choices from others and make your own\n"
          ]
        }
      ],
      "source": [
        "# Make a prediction on Tweet from the wild\n",
        "predict_on_sentence(model=model_6, # use the USE model\n",
        "                    sentence=daniels_tweet)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "tYOfNacw08Of"
      },
      "source": [
        "Woohoo! Our model predicted correctly. My Tweet wasn't about a diaster.\n",
        "\n",
        "How about we find a few Tweets about actual diasters?\n",
        "\n",
        "Such as the following two Tweets about the 2020 Beirut explosions."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 125,
      "metadata": {
        "id": "AqILBsTK2i9R"
      },
      "outputs": [],
      "source": [
        "# Source - https://twitter.com/BeirutCityGuide/status/1290696551376007168\n",
        "beirut_tweet_1 = \"Reports that the smoke in Beirut sky contains nitric acid, which is toxic. Please share and refrain from stepping outside unless urgent. #Lebanon\"\n",
        "\n",
        "# Source - https://twitter.com/BeirutCityGuide/status/1290773498743476224\n",
        "beirut_tweet_2 = \"#Beirut declared a “devastated city”, two-week state of emergency officially declared. #Lebanon\""
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 126,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "FvlbHDISrVmX",
        "outputId": "eb4dbb3d-0541-4094-f844-034436c7d12f"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "1/1 [==============================] - 0s 42ms/step\n",
            "Pred: 1.0 (real disaster) Prob: 0.9650391936302185\n",
            "Text:\n",
            "Reports that the smoke in Beirut sky contains nitric acid, which is toxic. Please share and refrain from stepping outside unless urgent. #Lebanon\n"
          ]
        }
      ],
      "source": [
        "# Predict on diaster Tweet 1\n",
        "predict_on_sentence(model=model_6, \n",
        "                    sentence=beirut_tweet_1)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 127,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "5uKYx11p2zCd",
        "outputId": "54de5279-bdab-4ea3-e24d-1e307f2a56a9"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "1/1 [==============================] - 0s 40ms/step\n",
            "Pred: 1.0 (real disaster) Prob: 0.9686568379402161\n",
            "Text:\n",
            "#Beirut declared a “devastated city”, two-week state of emergency officially declared. #Lebanon\n"
          ]
        }
      ],
      "source": [
        "# Predict on diaster Tweet 2\n",
        "predict_on_sentence(model=model_6, \n",
        "                    sentence=beirut_tweet_2)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "fczP1dFcwe98"
      },
      "source": [
        "Looks like our model is performing as expected, predicting both of the diaster Tweets as actual diasters.\n",
        "\n",
        "> 🔑 **Note:** The above examples are cherry-picked and are cases where you'd expect a model to function at high performance. For actual production systems, you'll want to continaully perform tests to see how your model is performing."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "Fp0fkK-tHPRE"
      },
      "source": [
        "## The speed/score tradeoff\n",
        "\n",
        "One of the final tests we're going to do is to find the speed/score tradeoffs between our best model and baseline model.\n",
        "\n",
        "Why is this important?\n",
        "\n",
        "Although it can be tempting to just choose the best performing model you find through experimentation, this model might not actually work in a production setting.\n",
        "\n",
        "Put it this way, imagine you're Twitter and receive 1 million Tweets per hour (this is a made up number, the actual number is much higher). And you're trying to build a diaster detection system to read Tweets and alert authorities with details about a diaster in close to real-time.\n",
        "\n",
        "Compute power isn't free so you're limited to a single compute machine for the project. On that machine, one of your models makes 10,000 predictions per second at 80% accuracy where as another one of your models (a larger model) makes 100 predictions per second at 85% accuracy.\n",
        "\n",
        "Which model do you choose?\n",
        "\n",
        "Is the second model's performance boost worth missing out on the extra capacity?\n",
        "\n",
        "Of course, there are many options you could try here, such as sending as many Tweets as possible to the first model and then sending the ones which the model is least certain of to the second model. \n",
        "\n",
        "The point here is to illustrate the best model you find through experimentation, might not be the model you end up using in production.\n",
        "\n",
        "To make this more concrete, let's write a function to take a model and a number of samples and time how long the given model takes to make predictions on those samples."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 128,
      "metadata": {
        "id": "DnXp8DKOp3J6"
      },
      "outputs": [],
      "source": [
        "# Calculate the time of predictions\n",
        "import time\n",
        "def pred_timer(model, samples):\n",
        "  \"\"\"\n",
        "  Times how long a model takes to make predictions on samples.\n",
        "  \n",
        "  Args:\n",
        "  ----\n",
        "  model = a trained model\n",
        "  sample = a list of samples\n",
        "\n",
        "  Returns:\n",
        "  ----\n",
        "  total_time = total elapsed time for model to make predictions on samples\n",
        "  time_per_pred = time in seconds per single sample\n",
        "  \"\"\"\n",
        "  start_time = time.perf_counter() # get start time\n",
        "  model.predict(samples) # make predictions\n",
        "  end_time = time.perf_counter() # get finish time\n",
        "  total_time = end_time-start_time # calculate how long predictions took to make\n",
        "  time_per_pred = total_time/len(val_sentences) # find prediction time per sample\n",
        "  return total_time, time_per_pred"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "GxWwS73hze6Z"
      },
      "source": [
        "Looking good!\n",
        "\n",
        "Now let's use our `pred_timer()` function to evaluate the prediction times of our best performing model (`model_6`) and our baseline model (`model_0`)."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 129,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "JMbGMIWd5c9N",
        "outputId": "af030ae6-9ae1-4f15-ca87-8daedba1c2d9"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "24/24 [==============================] - 0s 7ms/step\n"
          ]
        },
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "(0.2243557769999711, 0.0002944301535432692)"
            ]
          },
          "metadata": {},
          "execution_count": 129
        }
      ],
      "source": [
        "# Calculate TF Hub Sentence Encoder prediction times\n",
        "model_6_total_pred_time, model_6_time_per_pred = pred_timer(model_6, val_sentences)\n",
        "model_6_total_pred_time, model_6_time_per_pred"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 130,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "I4ej2VyT5oQs",
        "outputId": "09eb6fd5-3408-4959-a6d7-6fffdb532f19"
      },
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "(0.013254724999967493, 1.739465223092847e-05)"
            ]
          },
          "metadata": {},
          "execution_count": 130
        }
      ],
      "source": [
        "# Calculate Naive Bayes prediction times\n",
        "baseline_total_pred_time, baseline_time_per_pred = pred_timer(model_0, val_sentences)\n",
        "baseline_total_pred_time, baseline_time_per_pred"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "nqNnKMxhz8Kl"
      },
      "source": [
        "It seems with our current hardware (in my case, I'm using a Google Colab notebook) our best performing model takes over 10x the time to make predictions as our baseline model.\n",
        "\n",
        "Is that extra prediction time worth it?\n",
        "\n",
        "Let's compare time per prediction versus our model's F1-scores."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 131,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 641
        },
        "id": "ANKHEfRN7Nhd",
        "outputId": "450fd57d-430d-4dda-f711-1f58edd2291d"
      },
      "outputs": [
        {
          "output_type": "display_data",
          "data": {
            "text/plain": [
              "<Figure size 1000x700 with 1 Axes>"
            ],
            "image/png": "iVBORw0KGgoAAAANSUhEUgAAA2cAAAJwCAYAAAAN7xnyAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAABmLUlEQVR4nO3deVxXVeL/8fcHZF9dkMVQTB3TckkNcrckURuXyXIrRXP5mksqkynlmqnllFmm1jSWZppUWjYttrg0Zu6KpiKjhKmFqJmgGKJwfn/48zN9ZBEQ4xKv5+NxH/E599xzzr0Xgrf33nNtxhgjAAAAAECpcirtAQAAAAAACGcAAAAAYAmEMwAAAACwAMIZAAAAAFgA4QwAAAAALIBwBgAAAAAWQDgDAAAAAAsgnAEAAACABRDOAAAAAMACCGcAABTRgAEDFBYWVtrDwDU2bNggm82mDRs22MtK+lwtXrxYNptNR44cKbE2AeAqwhmAcuvqH1l5LRMmTLDX+/LLLzVo0CDdcccdcnZ25o/ycuLnn3/W1KlTFR8fX9pDQSmYOXOmPvroo9IeBoBypkJpDwAAStszzzyjmjVrOpTdcccd9q+XL1+uuLg4NWnSRCEhIX/08FBKfv75Z02bNk1hYWFq3Lixw7o33nhDOTk5pTMwFElxz9XMmTP14IMPqnv37g7l/fr1U+/eveXm5lZCIwSA/yGcASj3OnXqpGbNmuW7fubMmXrjjTfk4uKiv/71r9q3b98fOLqSkZGRIS8vr9IexnWVlXG6uLiU9hBKxc06Pzk5OcrKypK7u3uJt13S58rZ2VnOzs4l2iYAXMVtjQBwHSEhITf0B965c+c0ZswYhYWFyc3NTVWrVtV9992nXbt2OdTbunWrOnfurIoVK8rLy0sNGzbUyy+/7FBn3bp1at26tby8vOTv769u3bopISHBoc7UqVNls9l04MAB9e3bVxUrVlSrVq3s69955x01bdpUHh4eqlSpknr37q1jx44VuA8ffPCBbDabvvnmm1zrXn/9ddlsNofQevDgQT344IOqVKmS3N3d1axZM3388ccO2129rfSbb77R8OHDVbVqVd1yyy2FPmZhYWEaMGBArvG0a9dO7dq1cyibN2+ebr/9dnl6eqpixYpq1qyZli9fnu/+btiwQXfddZckaeDAgfbbXRcvXiwp93NMR44ckc1m0wsvvKD58+fr1ltvlaenpzp06KBjx47JGKPp06frlltukYeHh7p166YzZ87k6vfzzz+3n18fHx/df//92r9/f77jvPZY/uc//9H//d//qXLlyvL19VX//v3166+/FqufAQMGyNvbW0lJSercubN8fHz08MMP5zuGq993Bw8eVM+ePeXr66vKlStr9OjRyszMdKhrs9k0cuRILVu2TLfffrvc3Ny0Zs0aSdJPP/2kRx99VIGBgXJzc9Ptt9+uN998M1d/x48fV/fu3eXl5aWqVatq7NixunjxYq56eT1zlpOTo5dfflkNGjSQu7u7AgIC1LFjR+3YscM+voyMDC1ZssR+7q9+r+X3zNmCBQvs+xISEqIRI0bo7NmzDnXatWunO+64QwcOHNA999wjT09PVatWTbNnz873uAIoX7hyBqDcS0tL0+nTpx3KqlSpUmLtDxs2TB988IFGjhyp+vXr65dfftG3336rhIQENWnSRJL01Vdf6a9//auCg4M1evRoBQUFKSEhQZ988olGjx4tSfr666/VqVMn3XrrrZo6dap+++03zZs3Ty1bttSuXbty/QH60EMPqU6dOpo5c6aMMZKkGTNmaNKkSerZs6cGDx6sU6dOad68eWrTpo12794tf3//PPfh/vvvl7e3t9577z21bdvWYV1cXJxuv/12+62g+/fvV8uWLVWtWjVNmDBBXl5eeu+999S9e3etXLlSf/vb3xy2Hz58uAICAjR58mRlZGQU+pgV1htvvKHHH39cDz74oD0o7N27V1u3blXfvn3z3KZevXp65plnNHnyZA0dOlStW7eWJLVo0aLAvpYtW6asrCyNGjVKZ86c0ezZs9WzZ0/de++92rBhg8aPH6/Dhw9r3rx5euKJJxxCx9KlSxUdHa2oqCg9//zzunDhghYuXKhWrVpp9+7dhXrWceTIkfL399fUqVOVmJiohQsX6scff7RPlFHUfi5fvqyoqCi1atVKL7zwgjw9Pa87hp49eyosLEyzZs3Sli1b9Morr+jXX3/V22+/7VBv3bp1eu+99zRy5EhVqVJFYWFhSk1N1d13320PbwEBAfr88881aNAgpaena8yYMZKk3377Te3bt9fRo0f1+OOPKyQkREuXLtW6deuuOz5JGjRokBYvXqxOnTpp8ODBunz5sjZu3KgtW7aoWbNmWrp0qQYPHqzw8HAN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NXbsWLVp00YNGzYs1LiNMZLExCAAAABAOXc1E1zNCMVmLGDevHmmevXqxtXV1YSHh5stW7bY17Vt29ZER0c71D948KCRZL788ss820tJSTEDBgwwISEhxt3d3dStW9e8+OKLJicnxxhjzNGjR02bNm1MpUqVjJubm6ldu7YZN26cSUtLK/SYjx07ZiSxsLCwsLCwsLCwsLAYSebYsWNFD0O/YzPmRuNd+ZSTk6Off/5ZPj4+stlspT0clJCrE70cO3aM996VM5z78otzX75x/ssvzn35dTPOvTFG586dU0hISK7Hq4qi1G9rLKucnJx0yy23lPYwcJP4+vryP+pyinNffnHuyzfOf/nFuS+/Svrc+/n53XAbpTohCAAAAADgCsIZAAAAAFgA4Qz4HTc3N02ZMoV32pVDnPvyi3NfvnH+yy/Offll5XPPhCAAAAAAYAFcOQMAAAAACyCcAQAAAIAFEM4AAAAAwAIIZwAAAABgAYQzlBnz589XWFiY3N3dFRERoW3bthVY//3339dtt90md3d3NWjQQJ999pnDemOMJk+erODgYHl4eCgyMlKHDh1yqHPmzBk9/PDD8vX1lb+/vwYNGqTz58871Nm7d69at24td3d3hYaGavbs2Q7rFy9eLJvN5rC4u7vfwJEon6x4/jMzMzVgwAA1aNBAFSpUUPfu3fMcy4YNG9SkSRO5ubmpdu3aWrx4cbGOQXlVVs/9hg0bcv3s22w2nThxovgHo5yx4rnfsGGDunXrpuDgYHl5ealx48ZatmxZkceCgpXVc8/v/BtnxXOfmJioe+65R4GBgXJ3d9ett96qiRMn6tKlS0UaS6EYoAxYsWKFcXV1NW+++abZv3+/GTJkiPH39zepqal51t+0aZNxdnY2s2fPNgcOHDATJ040Li4u5vvvv7fXee6554yfn5/56KOPzJ49e0zXrl1NzZo1zW+//Wav07FjR9OoUSOzZcsWs3HjRlO7dm3Tp08f+/q0tDQTGBhoHn74YbNv3z7z7rvvGg8PD/P666/b67z11lvG19fXpKSk2JcTJ07chKP052XV83/+/HkzbNgw889//tNERUWZbt265RrLDz/8YDw9PU1MTIw5cOCAmTdvnnF2djZr1qwpuQP0J1aWz/369euNJJOYmOjw85+dnV1yB+hPzKrnfsaMGWbixIlm06ZN5vDhw2bu3LnGycnJ/Pvf/y7SWJC/snzu+Z1/Y6x67pOSksybb75p4uPjzZEjR8zq1atN1apVTWxsbJHGUhiEM5QJ4eHhZsSIEfbP2dnZJiQkxMyaNSvP+j179jT333+/Q1lERIT5v//7P2OMMTk5OSYoKMj84x//sK8/e/ascXNzM++++64xxpgDBw4YSWb79u32Op9//rmx2Wzmp59+MsYYs2DBAlOxYkVz8eJFe53x48ebunXr2j+/9dZbxs/Pr5h7DmOse/5/Lzo6Os8/0J988klz++23O5T16tXLREVFXWevYUzZPvdXw9mvv/5a6P3F/5SFc39V586dzcCBAws9FhSsLJ97fuffmLJ07seOHWtatWpV6LEUFrc1wvKysrK0c+dORUZG2sucnJwUGRmpzZs357nN5s2bHepLUlRUlL1+cnKyTpw44VDHz89PERER9jqbN2+Wv7+/mjVrZq8TGRkpJycnbd261V6nTZs2cnV1degnMTFRv/76q73s/PnzqlGjhkJDQ9WtWzft37+/uIej3LHy+S+M640F+Svr5/6qxo0bKzg4WPfdd582bdpU5O3Lo7J27tPS0lSpUqVCjwX5K+vnXuJ3fnGVpXN/+PBhrVmzRm3bti30WAqLcAbLO336tLKzsxUYGOhQHhgYmO+zGydOnCiw/tX/Xq9O1apVHdZXqFBBlSpVcqiTVxu/76Nu3bp68803tXr1ar3zzjvKyclRixYtdPz48cIdgHLOyue/MPIbS3p6un777bdCt1MelfVzHxwcrNdee00rV67UypUrFRoaqnbt2mnXrl2FbqO8Kkvn/r333tP27ds1cODAQo8F+Svr557f+cVXFs59ixYt5O7urjp16qh169Z65plnCj2WwqpQpNoAiqx58+Zq3ry5/XOLFi1Ur149vf7665o+fXopjgzAzVS3bl3VrVvX/rlFixZKSkrSSy+9pKVLl5biyFBS1q9fr4EDB+qNN97Q7bffXtrDwR8ov3PP7/w/t7i4OJ07d0579uzRuHHj9MILL+jJJ58s0T64cgbLq1KlipydnZWamupQnpqaqqCgoDy3CQoKKrD+1f9er87Jkycd1l++fFlnzpxxqJNXG7/v41ouLi668847dfjw4bx3GA6sfP4LI7+x+Pr6ysPDo9DtlEdl/dznJTw8nJ/9QigL5/6bb75Rly5d9NJLL6l///5FGgvyV9bP/bX4nV94ZeHch4aGqn79+urTp4+ee+45TZ06VdnZ2YUaS2ERzmB5rq6uatq0qdauXWsvy8nJ0dq1ax3+der3mjdv7lBfkr766it7/Zo1ayooKMihTnp6urZu3Wqv07x5c509e1Y7d+6011m3bp1ycnIUERFhr/Of//zHYSrVr776SnXr1lXFihXzHFt2dra+//57BQcHF+UwlFtWPv+Fcb2xIH9l/dznJT4+np/9QrD6ud+wYYPuv/9+Pf/88xo6dGiRx4L8lfVzfy1+5xee1c/9tXJycnTp0iXl5OQUaiyFVqTpQ4BSsmLFCuPm5mYWL15sDhw4YIYOHWr8/f3t09P269fPTJgwwV5/06ZNpkKFCuaFF14wCQkJZsqUKXlOrerv729Wr15t9u7da7p165bn1Kp33nmn2bp1q/n2229NnTp1HKZWPXv2rAkMDDT9+vUz+/btMytWrDCenp4OU+lPmzbNfPHFFyYpKcns3LnT9O7d27i7u5v9+/ffzEP2p2LV82+MMfv37ze7d+82Xbp0Me3atTO7d+82u3fvtq+/OpX+uHHjTEJCgpk/fz5T6RdBWT73L730kvnoo4/MoUOHzPfff29Gjx5tnJyczNdff32Tjtafi1XP/bp164ynp6eJjY11mC79l19+KdJYkL+yfO75nX9jrHru33nnHRMXF2cOHDhgkpKSTFxcnAkJCTEPP/xwkcZSGIQzlBnz5s0z1atXN66uriY8PNxs2bLFvq5t27YmOjraof57771n/vKXvxhXV1dz++23m08//dRhfU5Ojpk0aZIJDAw0bm5upn379iYxMdGhzi+//GL69OljvL29ja+vrxk4cKA5d+6cQ509e/aYVq1aGTc3N1OtWjXz3HPPOawfM2aMfdyBgYGmc+fOZteuXSVwRMoXq57/GjVqGEm5lt9bv369ady4sXF1dTW33nqreeutt278gJQjZfXcP//886ZWrVrG3d3dVKpUybRr186sW7euhI5K+WDFcx8dHZ3neW/btm2RxoKCldVzz+/8G2fFc79ixQrTpEkT4+3tbby8vEz9+vXNzJkzHQJeYcZSGDZjjCnatTYAAAAAQEnjmTMAAAAAsADCGQAAAABYAOEMAAAAACyAcAYAAAAAFkA4AwAAAAALIJwBAAAAgAUQzgAAAADAAghnAAAAAGABhDMAwE0xYMAAde/evbSH8adjs9n00UcfSZKOHDkim82m+Pj4YrdXEm0AAEpGhdIeAACg7LHZbAWunzJlil5++WUZY/6gEZVPoaGhSklJUZUqVQpVf8CAATp79qw93BWnDQDAzUM4AwAUWUpKiv3ruLg4TZ48WYmJifYyb29veXt7l8bQbppLly7JxcXFUm05OzsrKCio1NsAAJQMbmsEABRZUFCQffHz85PNZnMo8/b2znVbY7t27TRq1CiNGTNGFStWVGBgoN544w1lZGRo4MCB8vHxUe3atfX555879LVv3z516tRJ3t7eCgwMVL9+/XT69Ol8x7Z48WL5+/vro48+Up06deTu7q6oqCgdO3bMod7q1avVpEkTubu769Zbb9W0adN0+fJl+3qbzaaFCxeqa9eu8vLy0owZM/LsLywsTNOnT1efPn3k5eWlatWqaf78+Q518mvremM4dOiQ2rRpI3d3d9WvX19fffWVQ7t53ZK4f/9+/fWvf5Wvr698fHzUunVrJSUlaerUqVqyZIlWr14tm80mm82mDRs25NnGN998o/DwcLm5uSk4OFgTJkxwGFe7du30+OOP68knn1SlSpUUFBSkqVOn5ntOAACFQzgDAPxhlixZoipVqmjbtm0aNWqUHnvsMT300ENq0aKFdu3apQ4dOqhfv366cOGCJOns2bO69957deedd2rHjh1as2aNUlNT1bNnzwL7uXDhgmbMmKG3335bmzZt0tmzZ9W7d2/7+o0bN6p///4aPXq0Dhw4oNdff12LFy/OFcCmTp2qv/3tb/r+++/16KOP5tvfP/7xDzVq1Ei7d+/WhAkTNHr06FxB6tq2rjeGnJwcPfDAA3J1ddXWrVv12muvafz48QXu908//aQ2bdrIzc1N69at086dO/Xoo4/q8uXLeuKJJ9SzZ0917NhRKSkpSklJUYsWLfJso3Pnzrrrrru0Z88eLVy4UIsWLdKzzz7rUG/JkiXy8vLS1q1bNXv2bD3zzDO59hkAUEQGAIAb8NZbbxk/P79c5dHR0aZbt272z23btjWtWrWyf758+bLx8vIy/fr1s5elpKQYSWbz5s3GGGOmT59uOnTo4NDusWPHjCSTmJiY73gkmS1bttjLEhISjCSzdetWY4wx7du3NzNnznTYbunSpSY4ONj+WZIZM2bMdfbemBo1apiOHTs6lPXq1ct06tSpwLauN4YvvvjCVKhQwfz000/29Z9//rmRZD788ENjjDHJyclGktm9e7cxxpjY2FhTs2ZNk5WVledYrz0nebXx1FNPmbp165qcnBx7nfnz5xtvb2+TnZ1tjMl9Lo0x5q677jLjx4/Ps18AQOHwzBkA4A/TsGFD+9fOzs6qXLmyGjRoYC8LDAyUJJ08eVKStGfPHq1fvz7P59eSkpL0l7/8Jc9+KlSooLvuusv++bbbbpO/v78SEhIUHh6uPXv2aNOmTQ5XyrKzs5WZmakLFy7I09NTktSsWbNC7Vfz5s1zfZ47d65D2bVtXW8MCQkJCg0NVUhISL79XCs+Pl6tW7e+oefZEhIS1Lx5c4dJX1q2bKnz58/r+PHjql69uiTHcylJwcHB9vMGACgewhkA4A9zbWiw2WwOZVcDQU5OjiTp/Pnz6tKli55//vlcbQUHBxd7HOfPn9e0adP0wAMP5Frn7u5u/9rLy6vYfVzr2rYKO4ai8PDwKNZ2xZHXubx63gAAxUM4AwBYVpMmTbRy5UqFhYWpQoXC/8q6fPmyduzYofDwcElSYmKizp49q3r16tnbTUxMVO3atUtknFu2bMn1+Wpf+bneGOrVq6djx44pJSXFHkSv7edaDRs21JIlS/KdDdLV1VXZ2dkFtlGvXj2tXLlSxhh7WN60aZN8fHx0yy23FLgtAODGMCEIAMCyRowYoTNnzqhPnz7avn27kpKS9MUXX2jgwIEFhgwXFxeNGjVKW7du1c6dOzVgwADdfffd9rA2efJkvf3225o2bZr279+vhIQErVixQhMnTizWODdt2qTZs2frv//9r+bPn6/3339fo0ePLnCb640hMjJSf/nLXxQdHa09e/Zo48aNevrppwtsc+TIkUpPT1fv3r21Y8cOHTp0SEuXLrW/5iAsLEx79+5VYmKiTp8+rUuXLuVqY/jw4Tp27JhGjRqlgwcPavXq1ZoyZYpiYmLk5MSfDQBwM/F/WQCAZYWEhGjTpk3Kzs5Whw4d1KBBA40ZM0b+/v4FBgVPT0+NHz9effv2VcuWLeXt7a24uDj7+qioKH3yySf68ssvddddd+nuu+/WSy+9pBo1ahRrnH//+9+1Y8cO3XnnnXr22Wc1Z84cRUVFFbjN9cbg5OSkDz/8UL/99pvCw8M1ePDgfKfzv6py5cpat26dzp8/r7Zt26pp06Z644037FfRhgwZorp166pZs2YKCAjQpk2bcrVRrVo1ffbZZ9q2bZsaNWqkYcOGadCgQcUOrgCAwrMZY0xpDwIAgJKyePFijRkzRmfPnv1D+gsLC9OYMWM0ZsyYP6Q/AMCfF1fOAAAAAMACCGcAAAAAYAHc1ggAAAAAFsCVMwAAAACwAMIZAAAAAFgA4QwAAAAALIBwBgAAAAAWQDgDAAAAAAsgnAEAAACABRDOAAAAAMACCGcAAAAAYAH/D4xNKzPGvHXqAAAAAElFTkSuQmCC\n"
          },
          "metadata": {}
        }
      ],
      "source": [
        "import matplotlib.pyplot as plt\n",
        "\n",
        "plt.figure(figsize=(10, 7))\n",
        "plt.scatter(baseline_time_per_pred, baseline_results[\"f1\"], label=\"baseline\")\n",
        "plt.scatter(model_6_time_per_pred, model_6_results[\"f1\"], label=\"tf_hub_sentence_encoder\")\n",
        "plt.legend()\n",
        "plt.title(\"F1-score versus time per prediction\")\n",
        "plt.xlabel(\"Time per prediction\")\n",
        "plt.ylabel(\"F1-Score\");"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "QlHdTqTl0aOq"
      },
      "source": [
        "![](https://raw.githubusercontent.com/mrdbourke/tensorflow-deep-learning/main/images/08-ideal-performance-speed-of-pred-tradeoff-highlighted.png)\n",
        "*Ideal position for speed and performance tradeoff model (fast predictions with great results).*\n",
        "\n",
        "Of course, the ideal position for each of these dots is to be in the top left of the plot (low time per prediction, high F1-score). \n",
        "\n",
        "In our case, there's a clear tradeoff for time per prediction and performance. Our best performing model takes an order of magnitude longer per prediction but only results in a few F1-score point increase.\n",
        "\n",
        "This kind of tradeoff is something you'll need to keep in mind when incorporating machine learning models into your own applications."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "DJWGI6GpH4Gl"
      },
      "source": [
        "## 🛠 Exercises\n",
        "\n",
        "1. Rebuild, compile and train `model_1`, `model_2` and `model_5` using the [Keras Sequential API](https://www.tensorflow.org/api_docs/python/tf/keras/Sequential) instead of the Functional API.\n",
        "2. Retrain the baseline model with 10% of the training data. How does perform compared to the Universal Sentence Encoder model with 10% of the training data?\n",
        "3. Try fine-tuning the TF Hub Universal Sentence Encoder model by setting `training=True` when instantiating it as a Keras layer.\n",
        "\n",
        "```\n",
        "We can use this encoding layer in place of our text_vectorizer and embedding layer\n",
        "\n",
        "sentence_encoder_layer = hub.KerasLayer(\"https://tfhub.dev/google/universal-sentence-encoder/4\",\n",
        "                                        input_shape=[],\n",
        "                                        dtype=tf.string,\n",
        "                                        trainable=True) # turn training on to fine-tune the TensorFlow Hub model\n",
        "```\n",
        "4. Retrain the best model you've got so far on the whole training set (no validation split). Then use this trained model to make predictions on the test dataset and format the predictions into the same format as the `sample_submission.csv` file from Kaggle (see the Files tab in Colab for what the `sample_submission.csv` file looks like). Once you've done this, [make a submission to the Kaggle competition](https://www.kaggle.com/c/nlp-getting-started/data), how did your model perform?\n",
        "5. Combine the ensemble predictions using the majority vote (mode), how does this perform compare to averaging the prediction probabilities of each model?\n",
        "6. Make a confusion matrix with the best performing model's predictions on the validation set and the validation ground truth labels."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "BarVJji8H6M4"
      },
      "source": [
        "## 📖 Extra-curriculum \n",
        "\n",
        "To practice what you've learned, a good idea would be to spend an hour on 3 of the following (3-hours total, you could through them all if you want) and then write a blog post about what you've learned.\n",
        "\n",
        "* For an overview of the different problems within NLP and how to solve them read through: \n",
        " * [A Simple Introduction to Natural Language Processing](https://becominghuman.ai/a-simple-introduction-to-natural-language-processing-ea66a1747b32)\n",
        " * [How to solve 90% of NLP problems: a step-by-step guide](https://blog.insightdatascience.com/how-to-solve-90-of-nlp-problems-a-step-by-step-guide-fda605278e4e)\n",
        "* Go through [MIT's Recurrent Neural Networks lecture](https://youtu.be/SEnXr6v2ifU). This will be one of the greatest additions to what's happening behind the RNN model's you've been building.\n",
        "* Read through the [word embeddings page on the TensorFlow website](https://www.tensorflow.org/tutorials/text/word_embeddings). Embeddings are such a large part of NLP. We've covered them throughout this notebook but extra practice would be well worth it. A good exercise would be to write out all the code in the guide in a new notebook. \n",
        "* For more on RNN's in TensorFlow, read and reproduce [the TensorFlow RNN guide](https://www.tensorflow.org/guide/keras/rnn). We've covered many of the concepts in this guide, but it's worth writing the code again for yourself.\n",
        "* Text data doesn't always come in a nice package like the data we've downloaded. So if you're after more on preparing different text sources for being with your TensorFlow deep learning models, it's worth checking out the following:\n",
        " * [TensorFlow text loading tutorial](https://www.tensorflow.org/tutorials/load_data/text).\n",
        "  * [Reading text files with Python](https://realpython.com/read-write-files-python/) by Real Python.\n",
        "* This notebook has focused on writing NLP code. For a mathematically rich overview of how NLP with Deep Learning happens, read [Standford's Natural Language Processing with Deep Learning lecture notes Part 1](https://web.stanford.edu/class/cs224n/readings/cs224n-2019-notes01-wordvecs1.pdf).  \n",
        "  * For an even deeper dive, you could even do the whole [CS224n](http://web.stanford.edu/class/cs224n/) (Natural Language Processing with Deep Learning) course. \n",
        "* Great blog posts to read:\n",
        "  * Andrei Karpathy's [The Unreasonable Effectiveness of RNNs](https://karpathy.github.io/2015/05/21/rnn-effectiveness/) dives into generating Shakespeare text with RNNs.\n",
        "  * [Text Classification with NLP: Tf-Idf vs Word2Vec vs BERT](https://towardsdatascience.com/text-classification-with-nlp-tf-idf-vs-word2vec-vs-bert-41ff868d1794) by Mauro Di Pietro. An overview of different techniques for turning text into numbers and then classifying it.\n",
        "  * [What are word embeddings?](https://machinelearningmastery.com/what-are-word-embeddings/) by Machine Learning Mastery.\n",
        "* Other topics worth looking into:\n",
        "  * [Attention mechanisms](https://jalammar.github.io/visualizing-neural-machine-translation-mechanics-of-seq2seq-models-with-attention/). These are a foundational component of the transformer architecture and also often add improvments to deep NLP models.\n",
        "  * [Transformer architectures](http://jalammar.github.io/illustrated-transformer/). This model architecture has recently taken the NLP world by storm, achieving state of the art on many benchmarks. However, it does take a little more processing to get off the ground, the [HuggingFace Models (formerly HuggingFace Transformers) library](https://huggingface.co/models/) is probably your best quick start.\n",
        "    * And now [HuggingFace even have their own course](https://huggingface.co/course/chapter1) on how their library works! I haven't done it but anything HuggingFace makes is world-class.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "CLzfxgXkzEdr"
      },
      "source": [
        "> 📖 **Resource:** See the full set of course materials on GitHub: https://github.com/mrdbourke/tensorflow-deep-learning"
      ]
    }
  ],
  "metadata": {
    "accelerator": "GPU",
    "colab": {
      "provenance": [],
      "machine_shape": "hm",
      "gpuType": "A100",
      "include_colab_link": true
    },
    "gpuClass": "standard",
    "kernelspec": {
      "display_name": "Python 3 (ipykernel)",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "codemirror_mode": {
        "name": "ipython",
        "version": 3
      },
      "file_extension": ".py",
      "mimetype": "text/x-python",
      "name": "python",
      "nbconvert_exporter": "python",
      "pygments_lexer": "ipython3",
      "version": "3.9.7"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 0
}